Compare commits

...

21 Commits

Author SHA1 Message Date
8abf447d66 Merge branch 'main' of https://chookspace.com/SpookyDervish/mesh-network-project 2026-03-08 14:10:16 +11:00
12a8bd4c19 building out UI a ton more and also working on serialization and deserial for the desktop app 2026-03-08 14:09:43 +11:00
9195a461dd serialization and deserialization 2026-03-08 14:08:34 +11:00
0a560dca17 planning out api more 2026-03-08 07:40:27 +11:00
83c927d23a added pair screen, home screen, and channels screen 2026-03-08 06:56:18 +11:00
3bcab4414a changed look slightly 2026-03-07 14:34:25 +11:00
fe410f6444 work on ui 2026-03-07 14:22:06 +11:00
3aed2e5412 started writing api for desktop app 2026-03-07 10:38:18 +11:00
7deb752a97 WE DID IT I FIXED THE STUPID BLE HEADER AND IT WORKED 2026-03-07 10:18:47 +11:00
6bf4de8d1f idfk whats going on 2026-03-07 10:07:41 +11:00
b93ec675c4 only search for our specific service id 2026-03-07 10:06:33 +11:00
7c5f11be08 sigh im so stupid 2026-03-07 09:55:38 +11:00
6a13d718f4 fix AGAIN 2026-03-07 09:54:19 +11:00
bee510e9bd fix 2026-03-07 09:53:10 +11:00
78a931b535 get more bluetooth info 2026-03-07 09:52:08 +11:00
0f6cdabd25 tryna get bluetooth to work 2026-03-07 09:15:45 +11:00
b19ad3ce82 tiny code cleanup 2026-03-07 08:43:54 +11:00
7aa331c6d4 tryna get bluetooth to work 2026-03-07 08:40:56 +11:00
166819acce decided the relay will be coded in python 2026-03-04 16:31:57 +11:00
98bb4fcc36 decided on final look 2026-03-04 10:36:34 +11:00
14517a4ba3 make graph visualizer better 2026-03-04 10:23:05 +11:00
36 changed files with 3668 additions and 206 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
venv
*.pyc

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"micropico.syncFolder": "relay"
}

View File

@@ -0,0 +1,5 @@
class Channel:
def __init__(self, name: str, key: str, sf: int = 9):
self.name = name
self.key = key
self.sf = sf

View File

@@ -0,0 +1,6 @@
from api.node import MeshNode
class Message:
def __init__(self, content: str, sender: MeshNode):
self.content = content
self.sender = sender

80
desktop_app/api/node.py Normal file
View File

@@ -0,0 +1,80 @@
from bleak import BleakScanner, BleakClient
import asyncio
import struct
from dataclasses import dataclass
from enum import Enum
NODE_BLUETOOTH_SERVICE_UUID = "E1898FF7-5063-4441-a6eb-526073B00001"
NODE_BLUETOOTH_RX_UUID = "E1898FF7-5063-4441-a6eb-526073B00002"
NODE_BLUETOOTH_TX_UUID = "E1898FF7-5063-4441-a6eb-526073B00003"
class BluetoothPacketType(Enum):
SEND_MESSAGE = 1
GET_INFO = 2
@dataclass
class BluetoothPacket:
packet_type: BluetoothPacketType
data: dict
class MeshNode:
def __init__(self, client: BleakClient, app):
self.client = client
self.app = app
self.name = "None"
self.rx_queue = asyncio.Queue()
async def get_client_info(self):
await self.transmit_bluetooth_packet(BluetoothPacket(
BluetoothPacketType.GET_INFO,
None
))
def serialize_msg(self, packet: BluetoothPacket):
final = bytes()
final += struct.pack("<B", packet.packet_type.value)
final += repr(packet.data).encode()
return final
async def transmit_bluetooth_packet(self, packet: BluetoothPacket):
await self.client.write_gatt_char(NODE_BLUETOOTH_TX_UUID, self.serialize_msg(packet))
async def receive(self):
return await self.rx_queue.get()
async def ping(self):
await self.client.write_gatt_char(NODE_BLUETOOTH_TX_UUID, b"ping")
def notification_received(self, sender, data):
self.app.log(f"Receive: {data}")
self.rx_queue.put_nowait(data)
async def discover(app):
"""Find a mesh node via Bluetooth
"""
# find a nearby device that is a node
devices = await BleakScanner.discover(service_uuids=[NODE_BLUETOOTH_SERVICE_UUID], timeout=5)
# no device was found
if len(devices) == 0:
return None
# get the device using its address
device = await BleakScanner.find_device_by_address(devices[0].address, timeout=5)
client = BleakClient(device)
# connect to the device
await client.connect()
# make it so we can receive data
new_node = MeshNode(client, app)
await client.start_notify(NODE_BLUETOOTH_RX_UUID, new_node.notification_received)
# request info about the client
await new_node.get_client_info()
return new_node

7
desktop_app/main.py Normal file
View File

@@ -0,0 +1,7 @@
from api.node import MeshNode
from ui.app import mesh
if __name__ == "__main__":
app = mesh()
app.run()

18
desktop_app/ui/app.py Normal file
View File

@@ -0,0 +1,18 @@
from textual.app import App
from ui.screens.pair_screen import PairScreen
from api.node import MeshNode
from api.channel import Channel
class mesh(App):
CSS_PATH = "assets/global.tcss"
def __init__(self, driver_class = None, css_path = None, watch_css = False, ansi_color = False):
super().__init__(driver_class, css_path, watch_css, ansi_color)
self.mesh_node: MeshNode = None
# key = channel name
# value = channel
self.channels: dict[str, Channel]
def on_ready(self):
self.push_screen(PairScreen())

View File

@@ -0,0 +1,7 @@
_
| |
_ __ ___ ___ ___| |__
| '_ ` _ \ / _ \/ __| '_ \
| | | | | | __/\__ \ | | |
|_| |_| |_|\___||___/_| |_|

View File

@@ -0,0 +1,8 @@
_ _
| | | |
___| |__ __ _ _ __ _ __ ___| |___
/ __| '_ \ / _` | '_ \| '_ \ / _ \ / __|
| (__| | | | (_| | | | | | | | __/ \__ \
\___|_| |_|\__,_|_| |_|_| |_|\___|_|___/

View File

@@ -0,0 +1,9 @@
.banner {
padding: 1;
width: 100%;
background: $primary 50%;
color: $primary-lighten-1;
text-align: center;
margin: 1;
text-style: bold underline;
}

View File

@@ -0,0 +1,27 @@
PairScreen {
align: center middle;
EffectLabel {
min-width: 50;
text-align: center;
text-style: bold;
}
}
#middle {
border: $success round;
width: 45;
height: 15;
padding: 0 1 1 1;
Static {
min-width: 100%;
margin: 0 1;
}
LoadingIndicator {
height: 1;
margin-top: 1;
}
}

View File

@@ -0,0 +1,19 @@
from textual.screen import Screen
from textual.widgets import Header, Footer, ContentSwitcher
from ui.widgets.home_sidebar import HomeSidebar
from ui.widgets.home_info import HomeInfo
from ui.widgets.channels_list import ChannelsList
from ui.widgets.chat_window import ChatWindow
class MainScreen(Screen):
def compose(self):
yield Header(show_clock=True)
yield HomeSidebar()
with ContentSwitcher(initial="home-info"):
yield HomeInfo(id="home-info")
yield ChannelsList(id="channels-list")
yield ChatWindow(id="chat-window")
yield Footer()

View File

@@ -0,0 +1,41 @@
from textual.screen import Screen
from textual.containers import Vertical
from textual.widgets import Static, LoadingIndicator, DataTable
from textual import work
from ui.screens.main_screen import MainScreen
from textualeffects.widgets import EffectLabel
from api.node import MeshNode
class PairScreen(Screen):
CSS_PATH = "../assets/pair_screen.tcss"
@work
async def connect_to_node(self, is_retry = False):
if not is_retry:
self.notify("This may take a moment...", title="Discovering nearby nodes...")
self.app.mesh_node = await MeshNode.discover(self.app)
if self.app.mesh_node == None:
self.notify("Check your node is powered on and nearby.\nRetrying...", title="Failed to find a nearby node!", severity="warning")
return self.connect_to_node(True)
self.notify("Hurray! You're on the mesh!", title="Node connected!")
self.app.switch_screen(MainScreen())
async def on_compose(self):
self.connect_to_node()
def compose(self):
with Vertical(id="middle") as center_window:
center_window.border_title = "Pair a Node"
with open("ui/assets/banner.txt", "r") as f:
yield EffectLabel(f.read(), effect="Print")
yield Static("Attempting to connect to a nearby node. Make sure your mesh network node is powered and ready to pair.")
yield LoadingIndicator()

View File

@@ -0,0 +1,58 @@
from textual.containers import VerticalScroll, Vertical, HorizontalGroup
from textual.widgets import Static, Button, Rule, ContentSwitcher
from api.channel import Channel
class ChannelView(Button):
DEFAULT_CSS = """
ChannelView {
margin: 0 1;
background: $boost;
border: $surface-lighten-1 tall;
content-align: left middle;
text-align: left;
width: 30;
}
"""
def __init__(self, channel: Channel):
super().__init__(" [b]" + channel.name, flat=True)
self.channel = channel
class ChannelsList(Vertical):
DEFAULT_CSS = """
ChannelsList {
Rule {
color: $surface-lighten-1;
}
#buttons {
margin-bottom: 1;
Button {
margin: 0 1;
}
}
}
"""
def on_button_pressed(self, event: ChannelView.Pressed):
self.screen.query_one(ContentSwitcher).current = "chat-window"
def compose(self):
with VerticalScroll():
yield Static("channels", classes="banner")
yield ChannelView(Channel("test channel 1", "AQ=="))
yield ChannelView(Channel("test channel 2", "AQ=="))
yield ChannelView(Channel("test channel 3", "AQ=="))
yield ChannelView(Channel("test channel 4", "AQ=="))
yield ChannelView(Channel("test channel 5", "AQ=="))
yield ChannelView(Channel("test channel 6", "AQ=="))
yield ChannelView(Channel("test channel 7", "AQ=="))
yield Rule()
with HorizontalGroup(id="buttons"):
yield Button("Create Channel")
yield Button("Advertise")

View File

@@ -0,0 +1,82 @@
from textual.containers import Vertical, VerticalScroll, VerticalGroup, HorizontalGroup
from textual.widgets import Input, Button, Static
from api.message import Message
from api.node import MeshNode
class MessageView(VerticalGroup):
DEFAULT_CSS = """
MessageView {
margin-bottom: 1;
#message-text {
background: $surface;
padding: 1;
width: auto;
max-width: 25;
}
#triangle {
color: $surface;
width: auto;
}
&.right {
align-horizontal: right;
}
}
"""
def __init__(self, message: Message):
super().__init__()
self.message = message
if self.message.sender != self.app.mesh_node:
self.notify("right side")
self.add_class("right")
def compose(self):
user_name = self.message.sender.name
if self.message.sender == self.app.mesh_node:
user_name += " (You)"
yield Static(f"[b u cyan]{user_name}[/]\n{self.message.content}", id="message-text")
yield Static("", id="triangle")
class ChatWindow(Vertical):
DEFAULT_CSS = """
ChatWindow {
#message-history {
padding: 1;
}
#message-box {
margin-right: 2;
align: left middle;
#message-input {
margin: 1;
width: 90%;
}
#send-btn {
max-width: 10%;
margin: 1 0;
}
}
}
"""
def compose(self):
with VerticalScroll(id="message-history"):
fake = MeshNode(None, self.app)
fake.name = "billy"
yield MessageView(Message("hi!!!", fake))
yield MessageView(Message("hi!!!", fake))
yield MessageView(Message("hi!!!", self.app.mesh_node))
with HorizontalGroup(id="message-box"):
yield Input(placeholder="Send a message", id="message-input")
yield Button("", flat=True, id="send-btn")

View File

@@ -0,0 +1,25 @@
from textual.containers import Center
from textual.widgets import Static
from textualeffects.widgets import EffectLabel
class HomeInfo(Center):
DEFAULT_CSS = """
HomeInfo {
width: 100%;
padding: 0 3;
EffectLabel {
min-width: 100%;
text-align: center;
}
}
"""
def compose(self):
with open("ui/assets/banner.txt", "r") as f:
yield EffectLabel(f.read(), effect="Print")
yield Static("[cyan]󰍦[/] [b blink]1[/] new message(s)")
yield Static("[cyan]󰑩[/] [b]2 of 3[/] nodes online")
yield Static("[lime]󰢾[/] [b]SNR:[/] 10.0 dBm [b]| RSSI:[/] -115.0 dBm")

View File

@@ -0,0 +1,39 @@
from textual.containers import Vertical
from textual.widgets import Button, ContentSwitcher
class SidebarButton(Button):
DEFAULT_CSS = """
SidebarButton {
max-width: 100%;
margin-bottom: 1;
}
"""
class HomeSidebar(Vertical):
DEFAULT_CSS = """
HomeSidebar {
width: 11;
background: $boost;
border-right: $surface-lighten-1 tall;
padding: 1;
dock: left;
margin-top: 1;
}
"""
def on_button_pressed(self, event: Button.Pressed):
content_switcher: ContentSwitcher = self.screen.query_one(ContentSwitcher)
match event.button.id:
case "home-btn":
content_switcher.current = "home-info"
case "channels-btn":
content_switcher.current = "channels-list"
case "settings-btn":
content_switcher.current = "settings"
def compose(self):
yield SidebarButton("", tooltip="Home", id="home-btn")
yield SidebarButton("", tooltip="Channels", id="channels-btn")
yield SidebarButton("", tooltip="Settings", id="settings-btn")

View File

@@ -1,29 +0,0 @@
#pragma once
#include <stdint.h>
/*
This is the struct that gets packed and then sent over LoRa.
It's packed and uses stdint types because different hardware or compilers may attempt to
optimize it or change the size of different things, completely stuffing up and corrupting
our data. Endianess also matters.
Version 1:
Message Types:
- message: send a message via plain text to someone
- hello: announce yourself to other nodes, payload should include:
bytes 1-4: node id
byte 5: battery level or 255 for unkown
byte 6: name length
continuing bytes: name
*/
typedef struct {
uint8_t version; // version number to prevent breaking older packet formats
uint8_t ttl; // the number of hops left in the lifespan of the packet, prevents infinite hopping
uint8_t packetType; // packet type (message is the only type right now)
uint32_t senderId; // unique id of the person who sent the packet
uint32_t targetId; // 0xFFFFFFFF = broadcast
uint32_t messageId; // we can ignore packets if we've seen them twice
uint16_t payloadLength; // length of data
uint8_t payload[]; // actual data
} __attribute__((packed)) Packet;

1
relay/.gitignore vendored
View File

@@ -1 +0,0 @@
build

3
relay/.micropico Normal file
View File

@@ -0,0 +1,3 @@
{
"info": "This file is just used to identify a project folder."
}

View File

@@ -1,19 +0,0 @@
cmake_minimum_required(VERSION 4.2)
include(pico_sdk_import.cmake)
project(relay C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(PICO_EXAMPLES_PATH $(PROJECT_SOURCE_DIR))
pico_sdk_init()
add_executable(relay
main.c
)
target_link_libraries(relay pico_stdlib)
pico_add_extra_outputs(relay)

414
relay/_sx126x.py Normal file
View File

@@ -0,0 +1,414 @@
from sys import implementation
if implementation.name == 'micropython':
from utime import sleep_ms
if implementation.name == 'circuitpython':
from time import sleep
def sleep_ms(ms):
sleep(ms/1000)
def ASSERT(state):
assert state == ERR_NONE, ERROR[state]
def yield_():
sleep_ms(1)
SX126X_FREQUENCY_STEP_SIZE = 0.9536743164
SX126X_MAX_PACKET_LENGTH = const(255)
SX126X_CRYSTAL_FREQ = 32.0
SX126X_DIV_EXPONENT = const(25)
SX126X_CMD_NOP = const(0x00)
SX126X_CMD_SET_SLEEP = const(0x84)
SX126X_CMD_SET_STANDBY = const(0x80)
SX126X_CMD_SET_FS = const(0xC1)
SX126X_CMD_SET_TX = const(0x83)
SX126X_CMD_SET_RX = const(0x82)
SX126X_CMD_STOP_TIMER_ON_PREAMBLE = const(0x9F)
SX126X_CMD_SET_RX_DUTY_CYCLE = const(0x94)
SX126X_CMD_SET_CAD = const(0xC5)
SX126X_CMD_SET_TX_CONTINUOUS_WAVE = const(0xD1)
SX126X_CMD_SET_TX_INFINITE_PREAMBLE = const(0xD2)
SX126X_CMD_SET_REGULATOR_MODE = const(0x96)
SX126X_CMD_CALIBRATE = const(0x89)
SX126X_CMD_CALIBRATE_IMAGE = const(0x98)
SX126X_CMD_SET_PA_CONFIG = const(0x95)
SX126X_CMD_SET_RX_TX_FALLBACK_MODE = const(0x93)
SX126X_CMD_WRITE_REGISTER = const(0x0D)
SX126X_CMD_READ_REGISTER = const(0x1D)
SX126X_CMD_WRITE_BUFFER = const(0x0E)
SX126X_CMD_READ_BUFFER = const(0x1E)
SX126X_CMD_SET_DIO_IRQ_PARAMS = const(0x08)
SX126X_CMD_GET_IRQ_STATUS = const(0x12)
SX126X_CMD_CLEAR_IRQ_STATUS = const(0x02)
SX126X_CMD_SET_DIO2_AS_RF_SWITCH_CTRL = const(0x9D)
SX126X_CMD_SET_DIO3_AS_TCXO_CTRL = const(0x97)
SX126X_CMD_SET_RF_FREQUENCY = const(0x86)
SX126X_CMD_SET_PACKET_TYPE = const(0x8A)
SX126X_CMD_GET_PACKET_TYPE = const(0x11)
SX126X_CMD_SET_TX_PARAMS = const(0x8E)
SX126X_CMD_SET_MODULATION_PARAMS = const(0x8B)
SX126X_CMD_SET_PACKET_PARAMS = const(0x8C)
SX126X_CMD_SET_CAD_PARAMS = const(0x88)
SX126X_CMD_SET_BUFFER_BASE_ADDRESS = const(0x8F)
SX126X_CMD_SET_LORA_SYMB_NUM_TIMEOUT = const(0x0A)
SX126X_CMD_GET_STATUS = const(0xC0)
SX126X_CMD_GET_RSSI_INST = const(0x15)
SX126X_CMD_GET_RX_BUFFER_STATUS = const(0x13)
SX126X_CMD_GET_PACKET_STATUS = const(0x14)
SX126X_CMD_GET_DEVICE_ERRORS = const(0x17)
SX126X_CMD_CLEAR_DEVICE_ERRORS = const(0x07)
SX126X_CMD_GET_STATS = const(0x10)
SX126X_CMD_RESET_STATS = const(0x00)
SX126X_REG_WHITENING_INITIAL_MSB = const(0x06B8)
SX126X_REG_WHITENING_INITIAL_LSB = const(0x06B9)
SX126X_REG_CRC_INITIAL_MSB = const(0x06BC)
SX126X_REG_CRC_INITIAL_LSB = const(0x06BD)
SX126X_REG_CRC_POLYNOMIAL_MSB = const(0x06BE)
SX126X_REG_CRC_POLYNOMIAL_LSB = const(0x06BF)
SX126X_REG_SYNC_WORD_0 = const(0x06C0)
SX126X_REG_SYNC_WORD_1 = const(0x06C1)
SX126X_REG_SYNC_WORD_2 = const(0x06C2)
SX126X_REG_SYNC_WORD_3 = const(0x06C3)
SX126X_REG_SYNC_WORD_4 = const(0x06C4)
SX126X_REG_SYNC_WORD_5 = const(0x06C5)
SX126X_REG_SYNC_WORD_6 = const(0x06C6)
SX126X_REG_SYNC_WORD_7 = const(0x06C7)
SX126X_REG_NODE_ADDRESS = const(0x06CD)
SX126X_REG_BROADCAST_ADDRESS = const(0x06CE)
SX126X_REG_LORA_SYNC_WORD_MSB = const(0x0740)
SX126X_REG_LORA_SYNC_WORD_LSB = const(0x0741)
SX126X_REG_RANDOM_NUMBER_0 = const(0x0819)
SX126X_REG_RANDOM_NUMBER_1 = const(0x081A)
SX126X_REG_RANDOM_NUMBER_2 = const(0x081B)
SX126X_REG_RANDOM_NUMBER_3 = const(0x081C)
SX126X_REG_RX_GAIN = const(0x08AC)
SX126X_REG_OCP_CONFIGURATION = const(0x08E7)
SX126X_REG_XTA_TRIM = const(0x0911)
SX126X_REG_XTB_TRIM = const(0x0912)
SX126X_REG_SENSITIVITY_CONFIG = const(0x0889)
SX126X_REG_TX_CLAMP_CONFIG = const(0x08D8)
SX126X_REG_RTC_STOP = const(0x0920)
SX126X_REG_RTC_EVENT = const(0x0944)
SX126X_REG_IQ_CONFIG = const(0x0736)
SX126X_REG_RX_GAIN_RETENTION_0 = const(0x029F)
SX126X_REG_RX_GAIN_RETENTION_1 = const(0x02A0)
SX126X_REG_RX_GAIN_RETENTION_2 = const(0x02A1)
SX126X_SLEEP_START_COLD = const(0b00000000)
SX126X_SLEEP_START_WARM = const(0b00000100)
SX126X_SLEEP_RTC_OFF = const(0b00000000)
SX126X_SLEEP_RTC_ON = const(0b00000001)
SX126X_STANDBY_RC = const(0x00)
SX126X_STANDBY_XOSC = const(0x01)
SX126X_RX_TIMEOUT_NONE = const(0x000000)
SX126X_RX_TIMEOUT_INF = const(0xFFFFFF)
SX126X_TX_TIMEOUT_NONE = const(0x000000)
SX126X_STOP_ON_PREAMBLE_OFF = const(0x00)
SX126X_STOP_ON_PREAMBLE_ON = const(0x01)
SX126X_REGULATOR_LDO = const(0x00)
SX126X_REGULATOR_DC_DC = const(0x01)
SX126X_CALIBRATE_IMAGE_OFF = const(0b00000000)
SX126X_CALIBRATE_IMAGE_ON = const(0b01000000)
SX126X_CALIBRATE_ADC_BULK_P_OFF = const(0b00000000)
SX126X_CALIBRATE_ADC_BULK_P_ON = const(0b00100000)
SX126X_CALIBRATE_ADC_BULK_N_OFF = const(0b00000000)
SX126X_CALIBRATE_ADC_BULK_N_ON = const(0b00010000)
SX126X_CALIBRATE_ADC_PULSE_OFF = const(0b00000000)
SX126X_CALIBRATE_ADC_PULSE_ON = const(0b00001000)
SX126X_CALIBRATE_PLL_OFF = const(0b00000000)
SX126X_CALIBRATE_PLL_ON = const(0b00000100)
SX126X_CALIBRATE_RC13M_OFF = const(0b00000000)
SX126X_CALIBRATE_RC13M_ON = const(0b00000010)
SX126X_CALIBRATE_RC64K_OFF = const(0b00000000)
SX126X_CALIBRATE_RC64K_ON = const(0b00000001)
SX126X_CALIBRATE_ALL = const(0b01111111)
SX126X_CAL_IMG_430_MHZ_1 = const(0x6B)
SX126X_CAL_IMG_430_MHZ_2 = const(0x6F)
SX126X_CAL_IMG_470_MHZ_1 = const(0x75)
SX126X_CAL_IMG_470_MHZ_2 = const(0x81)
SX126X_CAL_IMG_779_MHZ_1 = const(0xC1)
SX126X_CAL_IMG_779_MHZ_2 = const(0xC5)
SX126X_CAL_IMG_863_MHZ_1 = const(0xD7)
SX126X_CAL_IMG_863_MHZ_2 = const(0xDB)
SX126X_CAL_IMG_902_MHZ_1 = const(0xE1)
SX126X_CAL_IMG_902_MHZ_2 = const(0xE9)
SX126X_PA_CONFIG_HP_MAX = const(0x07)
SX126X_PA_CONFIG_PA_LUT = const(0x01)
SX126X_PA_CONFIG_SX1262_8 = const(0x00)
SX126X_RX_TX_FALLBACK_MODE_FS = const(0x40)
SX126X_RX_TX_FALLBACK_MODE_STDBY_XOSC = const(0x30)
SX126X_RX_TX_FALLBACK_MODE_STDBY_RC = const(0x20)
SX126X_IRQ_TIMEOUT = const(0b1000000000)
SX126X_IRQ_CAD_DETECTED = const(0b0100000000)
SX126X_IRQ_CAD_DONE = const(0b0010000000)
SX126X_IRQ_CRC_ERR = const(0b0001000000)
SX126X_IRQ_HEADER_ERR = const(0b0000100000)
SX126X_IRQ_HEADER_VALID = const(0b0000010000)
SX126X_IRQ_SYNC_WORD_VALID = const(0b0000001000)
SX126X_IRQ_PREAMBLE_DETECTED = const(0b0000000100)
SX126X_IRQ_RX_DONE = const(0b0000000010)
SX126X_IRQ_TX_DONE = const(0b0000000001)
SX126X_IRQ_ALL = const(0b1111111111)
SX126X_IRQ_NONE = const(0b0000000000)
SX126X_DIO2_AS_IRQ = const(0x00)
SX126X_DIO2_AS_RF_SWITCH = const(0x01)
SX126X_DIO3_OUTPUT_1_6 = const(0x00)
SX126X_DIO3_OUTPUT_1_7 = const(0x01)
SX126X_DIO3_OUTPUT_1_8 = const(0x02)
SX126X_DIO3_OUTPUT_2_2 = const(0x03)
SX126X_DIO3_OUTPUT_2_4 = const(0x04)
SX126X_DIO3_OUTPUT_2_7 = const(0x05)
SX126X_DIO3_OUTPUT_3_0 = const(0x06)
SX126X_DIO3_OUTPUT_3_3 = const(0x07)
SX126X_PACKET_TYPE_GFSK = const(0x00)
SX126X_PACKET_TYPE_LORA = const(0x01)
SX126X_PA_RAMP_10U = const(0x00)
SX126X_PA_RAMP_20U = const(0x01)
SX126X_PA_RAMP_40U = const(0x02)
SX126X_PA_RAMP_80U = const(0x03)
SX126X_PA_RAMP_200U = const(0x04)
SX126X_PA_RAMP_800U = const(0x05)
SX126X_PA_RAMP_1700U = const(0x06)
SX126X_PA_RAMP_3400U = const(0x07)
SX126X_GFSK_FILTER_NONE = const(0x00)
SX126X_GFSK_FILTER_GAUSS_0_3 = const(0x08)
SX126X_GFSK_FILTER_GAUSS_0_5 = const(0x09)
SX126X_GFSK_FILTER_GAUSS_0_7 = const(0x0A)
SX126X_GFSK_FILTER_GAUSS_1 = const(0x0B)
SX126X_GFSK_RX_BW_4_8 = const(0x1F)
SX126X_GFSK_RX_BW_5_8 = const(0x17)
SX126X_GFSK_RX_BW_7_3 = const(0x0F)
SX126X_GFSK_RX_BW_9_7 = const(0x1E)
SX126X_GFSK_RX_BW_11_7 = const(0x16)
SX126X_GFSK_RX_BW_14_6 = const(0x0E)
SX126X_GFSK_RX_BW_19_5 = const(0x1D)
SX126X_GFSK_RX_BW_23_4 = const(0x15)
SX126X_GFSK_RX_BW_29_3 = const(0x0D)
SX126X_GFSK_RX_BW_39_0 = const(0x1C)
SX126X_GFSK_RX_BW_46_9 = const(0x14)
SX126X_GFSK_RX_BW_58_6 = const(0x0C)
SX126X_GFSK_RX_BW_78_2 = const(0x1B)
SX126X_GFSK_RX_BW_93_8 = const(0x13)
SX126X_GFSK_RX_BW_117_3 = const(0x0B)
SX126X_GFSK_RX_BW_156_2 = const(0x1A)
SX126X_GFSK_RX_BW_187_2 = const(0x12)
SX126X_GFSK_RX_BW_234_3 = const(0x0A)
SX126X_GFSK_RX_BW_312_0 = const(0x19)
SX126X_GFSK_RX_BW_373_6 = const(0x11)
SX126X_GFSK_RX_BW_467_0 = const(0x09)
SX126X_LORA_BW_7_8 = const(0x00)
SX126X_LORA_BW_10_4 = const(0x08)
SX126X_LORA_BW_15_6 = const(0x01)
SX126X_LORA_BW_20_8 = const(0x09)
SX126X_LORA_BW_31_25 = const(0x02)
SX126X_LORA_BW_41_7 = const(0x0A)
SX126X_LORA_BW_62_5 = const(0x03)
SX126X_LORA_BW_125_0 = const(0x04)
SX126X_LORA_BW_250_0 = const(0x05)
SX126X_LORA_BW_500_0 = const(0x06)
SX126X_LORA_CR_4_5 = const(0x01)
SX126X_LORA_CR_4_6 = const(0x02)
SX126X_LORA_CR_4_7 = const(0x03)
SX126X_LORA_CR_4_8 = const(0x04)
SX126X_LORA_LOW_DATA_RATE_OPTIMIZE_OFF = const(0x00)
SX126X_LORA_LOW_DATA_RATE_OPTIMIZE_ON = const(0x01)
SX126X_GFSK_PREAMBLE_DETECT_OFF = const(0x00)
SX126X_GFSK_PREAMBLE_DETECT_8 = const(0x04)
SX126X_GFSK_PREAMBLE_DETECT_16 = const(0x05)
SX126X_GFSK_PREAMBLE_DETECT_24 = const(0x06)
SX126X_GFSK_PREAMBLE_DETECT_32 = const(0x07)
SX126X_GFSK_ADDRESS_FILT_OFF = const(0x00)
SX126X_GFSK_ADDRESS_FILT_NODE = const(0x01)
SX126X_GFSK_ADDRESS_FILT_NODE_BROADCAST = const(0x02)
SX126X_GFSK_PACKET_FIXED = const(0x00)
SX126X_GFSK_PACKET_VARIABLE = const(0x01)
SX126X_GFSK_CRC_OFF = const(0x01)
SX126X_GFSK_CRC_1_BYTE = const(0x00)
SX126X_GFSK_CRC_2_BYTE = const(0x02)
SX126X_GFSK_CRC_1_BYTE_INV = const(0x04)
SX126X_GFSK_CRC_2_BYTE_INV = const(0x06)
SX126X_GFSK_WHITENING_OFF = const(0x00)
SX126X_GFSK_WHITENING_ON = const(0x01)
SX126X_LORA_HEADER_EXPLICIT = const(0x00)
SX126X_LORA_HEADER_IMPLICIT = const(0x01)
SX126X_LORA_CRC_OFF = const(0x00)
SX126X_LORA_CRC_ON = const(0x01)
SX126X_LORA_IQ_STANDARD = const(0x00)
SX126X_LORA_IQ_INVERTED = const(0x01)
SX126X_CAD_ON_1_SYMB = const(0x00)
SX126X_CAD_ON_2_SYMB = const(0x01)
SX126X_CAD_ON_4_SYMB = const(0x02)
SX126X_CAD_ON_8_SYMB = const(0x03)
SX126X_CAD_ON_16_SYMB = const(0x04)
SX126X_CAD_GOTO_STDBY = const(0x00)
SX126X_CAD_GOTO_RX = const(0x01)
SX126X_STATUS_MODE_STDBY_RC = const(0b00100000)
SX126X_STATUS_MODE_STDBY_XOSC = const(0b00110000)
SX126X_STATUS_MODE_FS = const(0b01000000)
SX126X_STATUS_MODE_RX = const(0b01010000)
SX126X_STATUS_MODE_TX = const(0b01100000)
SX126X_STATUS_DATA_AVAILABLE = const(0b00000100)
SX126X_STATUS_CMD_TIMEOUT = const(0b00000110)
SX126X_STATUS_CMD_INVALID = const(0b00001000)
SX126X_STATUS_CMD_FAILED = const(0b00001010)
SX126X_STATUS_TX_DONE = const(0b00001100)
SX126X_STATUS_SPI_FAILED = const(0b11111111)
SX126X_GFSK_RX_STATUS_PREAMBLE_ERR = const(0b10000000)
SX126X_GFSK_RX_STATUS_SYNC_ERR = const(0b01000000)
SX126X_GFSK_RX_STATUS_ADRS_ERR = const(0b00100000)
SX126X_GFSK_RX_STATUS_CRC_ERR = const(0b00010000)
SX126X_GFSK_RX_STATUS_LENGTH_ERR = const(0b00001000)
SX126X_GFSK_RX_STATUS_ABORT_ERR = const(0b00000100)
SX126X_GFSK_RX_STATUS_PACKET_RECEIVED = const(0b00000010)
SX126X_GFSK_RX_STATUS_PACKET_SENT = const(0b00000001)
SX126X_PA_RAMP_ERR = const(0b100000000)
SX126X_PLL_LOCK_ERR = const(0b001000000)
SX126X_XOSC_START_ERR = const(0b000100000)
SX126X_IMG_CALIB_ERR = const(0b000010000)
SX126X_ADC_CALIB_ERR = const(0b000001000)
SX126X_PLL_CALIB_ERR = const(0b000000100)
SX126X_RC13M_CALIB_ERR = const(0b000000010)
SX126X_RC64K_CALIB_ERR = const(0b000000001)
SX126X_SYNC_WORD_PUBLIC = const(0x34)
SX126X_SYNC_WORD_PRIVATE = const(0x12)
ERR_NONE = const(0)
ERR_UNKNOWN = const(-1)
ERR_CHIP_NOT_FOUND = const(-2)
ERR_MEMORY_ALLOCATION_FAILED = const(-3)
ERR_PACKET_TOO_LONG = const(-4)
ERR_TX_TIMEOUT = const(-5)
ERR_RX_TIMEOUT = const(-6)
ERR_CRC_MISMATCH = const(-7)
ERR_INVALID_BANDWIDTH = const(-8)
ERR_INVALID_SPREADING_FACTOR = const(-9)
ERR_INVALID_CODING_RATE = const(-10)
ERR_INVALID_BIT_RANGE = const(-11)
ERR_INVALID_FREQUENCY = const(-12)
ERR_INVALID_OUTPUT_POWER = const(-13)
PREAMBLE_DETECTED = const(-14)
CHANNEL_FREE = const(-15)
ERR_SPI_WRITE_FAILED = const(-16)
ERR_INVALID_CURRENT_LIMIT = const(-17)
ERR_INVALID_PREAMBLE_LENGTH = const(-18)
ERR_INVALID_GAIN = const(-19)
ERR_WRONG_MODEM = const(-20)
ERR_INVALID_NUM_SAMPLES = const(-21)
ERR_INVALID_RSSI_OFFSET = const(-22)
ERR_INVALID_ENCODING = const(-23)
ERR_INVALID_BIT_RATE = const(-101)
ERR_INVALID_FREQUENCY_DEVIATION = const(-102)
ERR_INVALID_BIT_RATE_BW_RATIO = const(-103)
ERR_INVALID_RX_BANDWIDTH = const(-104)
ERR_INVALID_SYNC_WORD = const(-105)
ERR_INVALID_DATA_SHAPING = const(-106)
ERR_INVALID_MODULATION = const(-107)
ERR_AT_FAILED = const(-201)
ERR_URL_MALFORMED = const(-202)
ERR_RESPONSE_MALFORMED_AT = const(-203)
ERR_RESPONSE_MALFORMED = const(-204)
ERR_MQTT_CONN_VERSION_REJECTED = const(-205)
ERR_MQTT_CONN_ID_REJECTED = const(-206)
ERR_MQTT_CONN_SERVER_UNAVAILABLE = const(-207)
ERR_MQTT_CONN_BAD_USERNAME_PASSWORD = const(-208)
ERR_MQTT_CONN_NOT_AUTHORIZED = const(-208)
ERR_MQTT_UNEXPECTED_PACKET_ID = const(-209)
ERR_MQTT_NO_NEW_PACKET_AVAILABLE = const(-210)
ERR_CMD_MODE_FAILED = const(-301)
ERR_FRAME_MALFORMED = const(-302)
ERR_FRAME_INCORRECT_CHECKSUM = const(-303)
ERR_FRAME_UNEXPECTED_ID = const(-304)
ERR_FRAME_NO_RESPONSE = const(-305)
ERR_INVALID_RTTY_SHIFT = const(-401)
ERR_UNSUPPORTED_ENCODING = const(-402)
ERR_INVALID_DATA_RATE = const(-501)
ERR_INVALID_ADDRESS_WIDTH = const(-502)
ERR_INVALID_PIPE_NUMBER = const(-503)
ERR_ACK_NOT_RECEIVED = const(-504)
ERR_INVALID_NUM_BROAD_ADDRS = const(-601)
ERR_INVALID_CRC_CONFIGURATION = const(-701)
LORA_DETECTED = const(-702)
ERR_INVALID_TCXO_VOLTAGE = const(-703)
ERR_INVALID_MODULATION_PARAMETERS = const(-704)
ERR_SPI_CMD_TIMEOUT = const(-705)
ERR_SPI_CMD_INVALID = const(-706)
ERR_SPI_CMD_FAILED = const(-707)
ERR_INVALID_SLEEP_PERIOD = const(-708)
ERR_INVALID_RX_PERIOD = const(-709)
ERR_INVALID_CALLSIGN = const(-801)
ERR_INVALID_NUM_REPEATERS = const(-802)
ERR_INVALID_REPEATER_CALLSIGN = const(-803)
ERR_INVALID_PACKET_TYPE = const(-804)
ERR_INVALID_PACKET_LENGTH = const(-805)
ERROR = {
0: 'ERR_NONE',
-1: 'ERR_UNKNOWN',
-2: 'ERR_CHIP_NOT_FOUND',
-3: 'ERR_MEMORY_ALLOCATION_FAILED',
-4: 'ERR_PACKET_TOO_LONG',
-5: 'ERR_TX_TIMEOUT',
-6: 'ERR_RX_TIMEOUT',
-7: 'ERR_CRC_MISMATCH',
-8: 'ERR_INVALID_BANDWIDTH',
-9: 'ERR_INVALID_SPREADING_FACTOR',
-10: 'ERR_INVALID_CODING_RATE',
-11: 'ERR_INVALID_BIT_RANGE',
-12: 'ERR_INVALID_FREQUENCY',
-13: 'ERR_INVALID_OUTPUT_POWER',
-14: 'PREAMBLE_DETECTED',
-15: 'CHANNEL_FREE',
-16: 'ERR_SPI_WRITE_FAILED',
-17: 'ERR_INVALID_CURRENT_LIMIT',
-18: 'ERR_INVALID_PREAMBLE_LENGTH',
-19: 'ERR_INVALID_GAIN',
-20: 'ERR_WRONG_MODEM',
-21: 'ERR_INVALID_NUM_SAMPLES',
-22: 'ERR_INVALID_RSSI_OFFSET',
-23: 'ERR_INVALID_ENCODING',
-101: 'ERR_INVALID_BIT_RATE',
-102: 'ERR_INVALID_FREQUENCY_DEVIATION',
-103: 'ERR_INVALID_BIT_RATE_BW_RATIO',
-104: 'ERR_INVALID_RX_BANDWIDTH',
-105: 'ERR_INVALID_SYNC_WORD',
-106: 'ERR_INVALID_DATA_SHAPING',
-107: 'ERR_INVALID_MODULATION',
-201: 'ERR_AT_FAILED',
-202: 'ERR_URL_MALFORMED',
-203: 'ERR_RESPONSE_MALFORMED_AT',
-204: 'ERR_RESPONSE_MALFORMED',
-205: 'ERR_MQTT_CONN_VERSION_REJECTED',
-206: 'ERR_MQTT_CONN_ID_REJECTED',
-207: 'ERR_MQTT_CONN_SERVER_UNAVAILABLE',
-208: 'ERR_MQTT_CONN_BAD_USERNAME_PASSWORD',
-208: 'ERR_MQTT_CONN_NOT_AUTHORIZED',
-209: 'ERR_MQTT_UNEXPECTED_PACKET_ID',
-210: 'ERR_MQTT_NO_NEW_PACKET_AVAILABLE',
-301: 'ERR_CMD_MODE_FAILED',
-302: 'ERR_FRAME_MALFORMED',
-303: 'ERR_FRAME_INCORRECT_CHECKSUM',
-304: 'ERR_FRAME_UNEXPECTED_ID',
-305: 'ERR_FRAME_NO_RESPONSE',
-401: 'ERR_INVALID_RTTY_SHIFT',
-402: 'ERR_UNSUPPORTED_ENCODING',
-501: 'ERR_INVALID_DATA_RATE',
-502: 'ERR_INVALID_ADDRESS_WIDTH',
-503: 'ERR_INVALID_PIPE_NUMBER',
-504: 'ERR_ACK_NOT_RECEIVED',
-601: 'ERR_INVALID_NUM_BROAD_ADDRS',
-701: 'ERR_INVALID_CRC_CONFIGURATION',
-702: 'LORA_DETECTED',
-703: 'ERR_INVALID_TCXO_VOLTAGE',
-704: 'ERR_INVALID_MODULATION_PARAMETERS',
-705: 'ERR_SPI_CMD_TIMEOUT',
-706: 'ERR_SPI_CMD_INVALID',
-707: 'ERR_SPI_CMD_FAILED',
-708: 'ERR_INVALID_SLEEP_PERIOD',
-709: 'ERR_INVALID_RX_PERIOD',
-801: 'ERR_INVALID_CALLSIGN',
-802: 'ERR_INVALID_NUM_REPEATERS',
-803: 'ERR_INVALID_REPEATER_CALLSIGN',
-804: 'ERR_INVALID_PACKET_TYPE',
-805: 'ERR_INVALID_PACKET_LENGTH'
}

120
relay/bluetooth_handler.py Normal file
View File

@@ -0,0 +1,120 @@
import bluetooth
import struct
from micropython import const
# i just made a UUID up and changed the last number to change what protocol you're using
SERVICE_UUID = bluetooth.UUID("E1898FF7-5063-4441-a6eb-526073B00001")
TX_UUID = bluetooth.UUID("E1898FF7-5063-4441-a6eb-526073B00002")
RX_UUID = bluetooth.UUID("E1898FF7-5063-4441-a6eb-526073B00003")
TX_CHAR = (TX_UUID, bluetooth.FLAG_NOTIFY)
RX_CHAR = (RX_UUID, bluetooth.FLAG_WRITE)
SERVICE = (SERVICE_UUID, (TX_CHAR, RX_CHAR))
IRQ_CONNECT = const(1)
IRQ_DISCONNECT = const(2)
IRQ_GATTS_WRITE = const(3)
# Advertising payloads are repeated packets of the following form:
# 1 byte data length (N + 1)
# 1 byte type (see constants below)
# N bytes type-specific data
_ADV_TYPE_FLAGS = const(0x01)
_ADV_TYPE_NAME = const(0x09)
_ADV_TYPE_UUID16_COMPLETE = const(0x3)
_ADV_TYPE_UUID32_COMPLETE = const(0x5)
_ADV_TYPE_UUID128_COMPLETE = const(0x7)
_ADV_TYPE_UUID16_MORE = const(0x2)
_ADV_TYPE_UUID32_MORE = const(0x4)
_ADV_TYPE_UUID128_MORE = const(0x6)
_ADV_TYPE_APPEARANCE = const(0x19)
_ADV_MAX_PAYLOAD = const(31)
class BluetoothHandler:
def __init__(self):
print("Initializing Bluetooth...")
self.ble = bluetooth.BLE()
print("Activating...")
self.ble.active(True)
print("Setting IRQ callback...")
self.ble.irq(self.irq)
print("Getting MAC address...")
self.mac_address = self._get_mac_address()
print(f"MAC address: {self.mac_address}")
self.ble.config(gap_name="NODE-" + self.mac_address)
((self.tx_handle, self.rx_handle),) = self.ble.gatts_register_services((SERVICE,))
self.connections = set()
self.advertise()
def deserialize_msg(self, s: bytes):
# returns packet type (int) and deserialized data
return s[0], eval(s[1:].decode())
def _get_mac_address(self):
mac = self.ble.config("mac")[1]
return ':'.join('{:02X}'.format(b) for b in mac)
def advertising_payload(limited_disc=False, br_edr=False, name=None, services=None, appearance=0):
payload = bytearray()
def _append(adv_type, value):
nonlocal payload
payload += struct.pack("BB", len(value) + 1, adv_type) + value
_append(
_ADV_TYPE_FLAGS,
struct.pack("B", (0x01 if limited_disc else 0x02) + (0x18 if br_edr else 0x04)),
)
if name:
_append(_ADV_TYPE_NAME, name)
if services:
for uuid in services:
b = bytes(uuid)
if len(b) == 2:
_append(_ADV_TYPE_UUID16_COMPLETE, b)
elif len(b) == 4:
_append(_ADV_TYPE_UUID32_COMPLETE, b)
elif len(b) == 16:
_append(_ADV_TYPE_UUID128_COMPLETE, b)
# See org.bluetooth.characteristic.gap.appearance.xml
if appearance:
_append(_ADV_TYPE_APPEARANCE, struct.pack("<h", appearance))
if len(payload) > _ADV_MAX_PAYLOAD:
raise ValueError("advertising payload too large")
return payload
def advertise(self):
print("Advertising Bluetooth...")
self.ble.gap_advertise(100_000, self.advertising_payload(name="MeshNode", services=[SERVICE_UUID]))
def irq(self, event, data):
print(f"BLUETOOTH IRQ | EVENT: {event}, DATA: {data}")
if event == IRQ_CONNECT:
conn_handle, _, _ = data
self.connections.add(conn_handle)
elif event == IRQ_DISCONNECT:
conn_handle, _, _ = data
self.connections.remove(conn_handle)
self.advertise()
elif event == IRQ_GATTS_WRITE:
conn_handle, value_handle = data
msg = self.ble.gatts_read(value_handle)
packet_type, msg = self.deserialize_msg(msg)
print(f"Received: \"{msg}\"")

17
relay/lora_handler.py Normal file
View File

@@ -0,0 +1,17 @@
class LoRaHandler:
def __init__(self):
print("Initializing LoRa...")
# initialize our radio, im using the HAT SX1262 hat for the pico
self.radio = SX1262(spi_bus=1, clk=10, mosi=11, miso=12, cs=3, irq=20, rst=15, gpio=2)
self.radio.begin(freq=915, bw=125, power=22)
self.radio.setBlockingCallback(False, self.irq)
def irq(self, events):
print(f"LORA EVENT: {events}")
if events & SX1262.RX_DONE:
msg, err = sx.recv()
error = SX1262.STATUS[err]
print('Receive: {}, {}'.format(msg, error))
elif events & SX1262.TX_DONE:
print('TX done.')

View File

@@ -1,23 +0,0 @@
#include "../include/packet.h"
#include <stdio.h>
#include "pico/stdlib.h"
#define LED_PIN 25
int main() {
gpio_init(LED_PIN);
gpio_set_dir(LED_PIN, GPIO_OUT);
stdio_init_all();
while (true) {
printf("hello!!!\n");
gpio_put(LED_PIN, 1);
sleep_ms(2000);
gpio_put(LED_PIN, 0);
sleep_ms(2000);
}
}

24
relay/main.py Normal file
View File

@@ -0,0 +1,24 @@
from sx1262 import SX1262
from _sx126x import *
import time
from bluetooth_handler import BluetoothHandler
from lora_handler import LoRaHandler
LORA_ENABLED = False
def main():
bluetooth_handler = BluetoothHandler()
lora_handler = None
if LORA_ENABLED:
lora_handler = LoRaHandler()
print("Halting Pico...")
while True:
pass
if __name__ == "__main__":
main()

View File

@@ -1,121 +0,0 @@
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
# This can be dropped into an external project to help locate this SDK
# It should be include()ed prior to project()
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
endif ()
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
endif()
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
if (NOT PICO_SDK_PATH)
if (PICO_SDK_FETCH_FROM_GIT)
include(FetchContent)
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
if (PICO_SDK_FETCH_FROM_GIT_PATH)
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
endif ()
FetchContent_Declare(
pico_sdk
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
)
if (NOT pico_sdk)
message("Downloading Raspberry Pi Pico SDK")
# GIT_SUBMODULES_RECURSE was added in 3.17
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
GIT_SUBMODULES_RECURSE FALSE
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
else ()
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
endif ()
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
endif ()
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
else ()
message(FATAL_ERROR
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
)
endif ()
endif ()
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
if (NOT EXISTS ${PICO_SDK_PATH})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
endif ()
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
endif ()
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
include(${PICO_SDK_INIT_CMAKE_FILE})

268
relay/sx1262.py Normal file
View File

@@ -0,0 +1,268 @@
from _sx126x import *
from sx126x import SX126X
_SX126X_PA_CONFIG_SX1262 = const(0x00)
class SX1262(SX126X):
TX_DONE = SX126X_IRQ_TX_DONE
RX_DONE = SX126X_IRQ_RX_DONE
ADDR_FILT_OFF = SX126X_GFSK_ADDRESS_FILT_OFF
ADDR_FILT_NODE = SX126X_GFSK_ADDRESS_FILT_NODE
ADDR_FILT_NODE_BROAD = SX126X_GFSK_ADDRESS_FILT_NODE_BROADCAST
PREAMBLE_DETECT_OFF = SX126X_GFSK_PREAMBLE_DETECT_OFF
PREAMBLE_DETECT_8 = SX126X_GFSK_PREAMBLE_DETECT_8
PREAMBLE_DETECT_16 = SX126X_GFSK_PREAMBLE_DETECT_16
PREAMBLE_DETECT_24 = SX126X_GFSK_PREAMBLE_DETECT_24
PREAMBLE_DETECT_32 = SX126X_GFSK_PREAMBLE_DETECT_32
STATUS = ERROR
def __init__(self, spi_bus, clk, mosi, miso, cs, irq, rst, gpio):
super().__init__(spi_bus, clk, mosi, miso, cs, irq, rst, gpio)
self._callbackFunction = self._dummyFunction
def begin(self, freq=434.0, bw=125.0, sf=9, cr=7, syncWord=SX126X_SYNC_WORD_PRIVATE,
power=14, currentLimit=60.0, preambleLength=8, implicit=False, implicitLen=0xFF,
crcOn=True, txIq=False, rxIq=False, tcxoVoltage=1.6, useRegulatorLDO=False,
blocking=True):
state = super().begin(bw, sf, cr, syncWord, currentLimit, preambleLength, tcxoVoltage, useRegulatorLDO, txIq, rxIq)
ASSERT(state)
if not implicit:
state = super().explicitHeader()
else:
state = super().implicitHeader(implicitLen)
ASSERT(state)
state = super().setCRC(crcOn)
ASSERT(state)
state = self.setFrequency(freq)
ASSERT(state)
state = self.setOutputPower(power)
ASSERT(state)
state = super().fixPaClamping()
ASSERT(state)
state = self.setBlockingCallback(blocking)
return state
def beginFSK(self, freq=434.0, br=48.0, freqDev=50.0, rxBw=156.2, power=14, currentLimit=60.0,
preambleLength=16, dataShaping=0.5, syncWord=[0x2D, 0x01], syncBitsLength=16,
addrFilter=SX126X_GFSK_ADDRESS_FILT_OFF, addr=0x00, crcLength=2, crcInitial=0x1D0F, crcPolynomial=0x1021,
crcInverted=True, whiteningOn=True, whiteningInitial=0x0100,
fixedPacketLength=False, packetLength=0xFF, preambleDetectorLength=SX126X_GFSK_PREAMBLE_DETECT_16,
tcxoVoltage=1.6, useRegulatorLDO=False,
blocking=True):
state = super().beginFSK(br, freqDev, rxBw, currentLimit, preambleLength, dataShaping, preambleDetectorLength, tcxoVoltage, useRegulatorLDO)
ASSERT(state)
state = super().setSyncBits(syncWord, syncBitsLength)
ASSERT(state)
if addrFilter == SX126X_GFSK_ADDRESS_FILT_OFF:
state = super().disableAddressFiltering()
elif addrFilter == SX126X_GFSK_ADDRESS_FILT_NODE:
state = super().setNodeAddress(addr)
elif addrFilter == SX126X_GFSK_ADDRESS_FILT_NODE_BROADCAST:
state = super().setBroadcastAddress(addr)
else:
state = ERR_UNKNOWN
ASSERT(state)
state = super().setCRC(crcLength, crcInitial, crcPolynomial, crcInverted)
ASSERT(state)
state = super().setWhitening(whiteningOn, whiteningInitial)
ASSERT(state)
if fixedPacketLength:
state = super().fixedPacketLengthMode(packetLength)
else:
state = super().variablePacketLengthMode(packetLength)
ASSERT(state)
state = self.setFrequency(freq)
ASSERT(state)
state = self.setOutputPower(power)
ASSERT(state)
state = super().fixPaClamping()
ASSERT(state)
state = self.setBlockingCallback(blocking)
return state
def setFrequency(self, freq, calibrate=True):
if freq < 150.0 or freq > 960.0:
return ERR_INVALID_FREQUENCY
state = ERR_NONE
if calibrate:
data = bytearray(2)
if freq > 900.0:
data[0] = SX126X_CAL_IMG_902_MHZ_1
data[1] = SX126X_CAL_IMG_902_MHZ_2
elif freq > 850.0:
data[0] = SX126X_CAL_IMG_863_MHZ_1
data[1] = SX126X_CAL_IMG_863_MHZ_2
elif freq > 770.0:
data[0] = SX126X_CAL_IMG_779_MHZ_1
data[1] = SX126X_CAL_IMG_779_MHZ_2
elif freq > 460.0:
data[0] = SX126X_CAL_IMG_470_MHZ_1
data[1] = SX126X_CAL_IMG_470_MHZ_2
else:
data[0] = SX126X_CAL_IMG_430_MHZ_1
data[1] = SX126X_CAL_IMG_430_MHZ_2
state = super().calibrateImage(data)
ASSERT(state)
return super().setFrequencyRaw(freq)
def setOutputPower(self, power):
if not ((power >= -9) and (power <= 22)):
return ERR_INVALID_OUTPUT_POWER
ocp = bytearray(1)
ocp_mv = memoryview(ocp)
state = super().readRegister(SX126X_REG_OCP_CONFIGURATION, ocp_mv, 1)
ASSERT(state)
state = super().setPaConfig(0x04, _SX126X_PA_CONFIG_SX1262)
ASSERT(state)
state = super().setTxParams(power)
ASSERT(state)
return super().writeRegister(SX126X_REG_OCP_CONFIGURATION, ocp, 1)
def setTxIq(self, txIq):
self._txIq = txIq
def setRxIq(self, rxIq):
self._rxIq = rxIq
if not self.blocking:
ASSERT(super().startReceive())
def setPreambleDetectorLength(self, preambleDetectorLength):
self._preambleDetectorLength = preambleDetectorLength
if not self.blocking:
ASSERT(super().startReceive())
def setBlockingCallback(self, blocking, callback=None):
self.blocking = blocking
if not self.blocking:
state = super().startReceive()
ASSERT(state)
if callback != None:
self._callbackFunction = callback
super().setDio1Action(self._onIRQ)
else:
self._callbackFunction = self._dummyFunction
super().clearDio1Action()
return state
else:
state = super().standby()
ASSERT(state)
self._callbackFunction = self._dummyFunction
super().clearDio1Action()
return state
def recv(self, len=0, timeout_en=False, timeout_ms=0):
if not self.blocking:
return self._readData(len)
else:
return self._receive(len, timeout_en, timeout_ms)
def send(self, data):
if not self.blocking:
return self._startTransmit(data)
else:
return self._transmit(data)
def _events(self):
return super().getIrqStatus()
def _receive(self, len_=0, timeout_en=False, timeout_ms=0):
state = ERR_NONE
length = len_
if len_ == 0:
length = SX126X_MAX_PACKET_LENGTH
data = bytearray(length)
data_mv = memoryview(data)
try:
state = super().receive(data_mv, length, timeout_en, timeout_ms)
except AssertionError as e:
state = list(ERROR.keys())[list(ERROR.values()).index(str(e))]
if state == ERR_NONE or state == ERR_CRC_MISMATCH:
if len_ == 0:
length = super().getPacketLength(False)
data = data[:length]
else:
return b'', state
return bytes(data), state
def _transmit(self, data):
if isinstance(data, bytes) or isinstance(data, bytearray):
pass
else:
return 0, ERR_INVALID_PACKET_TYPE
state = super().transmit(data, len(data))
return len(data), state
def _readData(self, len_=0):
state = ERR_NONE
length = super().getPacketLength()
if len_ < length and len_ != 0:
length = len_
data = bytearray(length)
data_mv = memoryview(data)
try:
state = super().readData(data_mv, length)
except AssertionError as e:
state = list(ERROR.keys())[list(ERROR.values()).index(str(e))]
ASSERT(super().startReceive())
if state == ERR_NONE or state == ERR_CRC_MISMATCH:
return bytes(data), state
else:
return b'', state
def _startTransmit(self, data):
if isinstance(data, bytes) or isinstance(data, bytearray):
pass
else:
return 0, ERR_INVALID_PACKET_TYPE
state = super().startTransmit(data, len(data))
return len(data), state
def _dummyFunction(self, *args):
pass
def _onIRQ(self, callback):
events = self._events()
if events & SX126X_IRQ_TX_DONE:
super().startReceive()
self._callbackFunction(events)

1421
relay/sx126x.py Normal file

File diff suppressed because it is too large Load Diff

46
test.py Normal file
View File

@@ -0,0 +1,46 @@
from bleak import BleakScanner, BleakClient
import asyncio
async def main():
devices = await BleakScanner.discover(service_uuids=["E1898FF7-5063-4441-a6eb-526073B00001"])
for device in devices:
print()
print(f"Name: {device.name}")
print(f"Address: {device.address}")
print(f"Details: {device.details}")
for device in devices:
try:
this_device = await BleakScanner.find_device_by_address(device.address, timeout=20)
async with BleakClient(this_device) as client:
print(f'Services found for device')
print(f'\tDevice address:{device.address}')
print(f'\tDevice name:{device.name}')
client.write_gatt_char()
print('\tServices:')
for service in client.services:
print()
print(f'\t\tDescription: {service.description}')
print(f'\t\tService: {service}')
print('\t\tCharacteristics:')
for c in service.characteristics:
print()
print(f'\t\t\tUUID: {c.uuid}'),
print(f'\t\t\tDescription: {c.uuid}')
print(f'\t\t\tHandle: {c.uuid}'),
print(f'\t\t\tProperties: {c.uuid}')
print('\t\tDescriptors:')
for descrip in c.descriptors:
print(f'\t\t\t{descrip}')
except Exception as e:
print(f"Could not connect to device with info: {device}")
print(f"Error: {e}")
asyncio.run(main())

View File

@@ -0,0 +1,189 @@
function neighbourhoodHighlight(params) {
// console.log("in nieghbourhoodhighlight");
allNodes = nodes.get({ returnType: "Object" });
// originalNodes = JSON.parse(JSON.stringify(allNodes));
// if something is selected:
if (params.nodes.length > 0) {
highlightActive = true;
var i, j;
var selectedNode = params.nodes[0];
var degrees = 2;
// mark all nodes as hard to read.
for (let nodeId in allNodes) {
// nodeColors[nodeId] = allNodes[nodeId].color;
allNodes[nodeId].color = "rgba(200,200,200,0.5)";
if (allNodes[nodeId].hiddenLabel === undefined) {
allNodes[nodeId].hiddenLabel = allNodes[nodeId].label;
allNodes[nodeId].label = undefined;
}
}
var connectedNodes = network.getConnectedNodes(selectedNode);
var allConnectedNodes = [];
// get the second degree nodes
for (i = 1; i < degrees; i++) {
for (j = 0; j < connectedNodes.length; j++) {
allConnectedNodes = allConnectedNodes.concat(
network.getConnectedNodes(connectedNodes[j])
);
}
}
// all second degree nodes get a different color and their label back
for (i = 0; i < allConnectedNodes.length; i++) {
// allNodes[allConnectedNodes[i]].color = "pink";
allNodes[allConnectedNodes[i]].color = "rgba(150,150,150,0.75)";
if (allNodes[allConnectedNodes[i]].hiddenLabel !== undefined) {
allNodes[allConnectedNodes[i]].label =
allNodes[allConnectedNodes[i]].hiddenLabel;
allNodes[allConnectedNodes[i]].hiddenLabel = undefined;
}
}
// all first degree nodes get their own color and their label back
for (i = 0; i < connectedNodes.length; i++) {
// allNodes[connectedNodes[i]].color = undefined;
allNodes[connectedNodes[i]].color = nodeColors[connectedNodes[i]];
if (allNodes[connectedNodes[i]].hiddenLabel !== undefined) {
allNodes[connectedNodes[i]].label =
allNodes[connectedNodes[i]].hiddenLabel;
allNodes[connectedNodes[i]].hiddenLabel = undefined;
}
}
// the main node gets its own color and its label back.
// allNodes[selectedNode].color = undefined;
allNodes[selectedNode].color = nodeColors[selectedNode];
if (allNodes[selectedNode].hiddenLabel !== undefined) {
allNodes[selectedNode].label = allNodes[selectedNode].hiddenLabel;
allNodes[selectedNode].hiddenLabel = undefined;
}
} else if (highlightActive === true) {
// console.log("highlightActive was true");
// reset all nodes
for (let nodeId in allNodes) {
// allNodes[nodeId].color = "purple";
allNodes[nodeId].color = nodeColors[nodeId];
// delete allNodes[nodeId].color;
if (allNodes[nodeId].hiddenLabel !== undefined) {
allNodes[nodeId].label = allNodes[nodeId].hiddenLabel;
allNodes[nodeId].hiddenLabel = undefined;
}
}
highlightActive = false;
}
// transform the object into an array
var updateArray = [];
if (params.nodes.length > 0) {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
// console.log(allNodes[nodeId]);
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
} else {
// console.log("Nothing was selected");
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
// console.log(allNodes[nodeId]);
// allNodes[nodeId].color = {};
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
}
}
function filterHighlight(params) {
allNodes = nodes.get({ returnType: "Object" });
// if something is selected:
if (params.nodes.length > 0) {
filterActive = true;
let selectedNodes = params.nodes;
// hiding all nodes and saving the label
for (let nodeId in allNodes) {
allNodes[nodeId].hidden = true;
if (allNodes[nodeId].savedLabel === undefined) {
allNodes[nodeId].savedLabel = allNodes[nodeId].label;
allNodes[nodeId].label = undefined;
}
}
for (let i=0; i < selectedNodes.length; i++) {
allNodes[selectedNodes[i]].hidden = false;
if (allNodes[selectedNodes[i]].savedLabel !== undefined) {
allNodes[selectedNodes[i]].label = allNodes[selectedNodes[i]].savedLabel;
allNodes[selectedNodes[i]].savedLabel = undefined;
}
}
} else if (filterActive === true) {
// reset all nodes
for (let nodeId in allNodes) {
allNodes[nodeId].hidden = false;
if (allNodes[nodeId].savedLabel !== undefined) {
allNodes[nodeId].label = allNodes[nodeId].savedLabel;
allNodes[nodeId].savedLabel = undefined;
}
}
filterActive = false;
}
// transform the object into an array
var updateArray = [];
if (params.nodes.length > 0) {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
} else {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
}
}
function selectNode(nodes) {
network.selectNodes(nodes);
neighbourhoodHighlight({ nodes: nodes });
return nodes;
}
function selectNodes(nodes) {
network.selectNodes(nodes);
filterHighlight({nodes: nodes});
return nodes;
}
function highlightFilter(filter) {
let selectedNodes = []
let selectedProp = filter['property']
if (filter['item'] === 'node') {
let allNodes = nodes.get({ returnType: "Object" });
for (let nodeId in allNodes) {
if (allNodes[nodeId][selectedProp] && filter['value'].includes((allNodes[nodeId][selectedProp]).toString())) {
selectedNodes.push(nodeId)
}
}
}
else if (filter['item'] === 'edge'){
let allEdges = edges.get({returnType: 'object'});
// check if the selected property exists for selected edge and select the nodes connected to the edge
for (let edge in allEdges) {
if (allEdges[edge][selectedProp] && filter['value'].includes((allEdges[edge][selectedProp]).toString())) {
selectedNodes.push(allEdges[edge]['from'])
selectedNodes.push(allEdges[edge]['to'])
}
}
}
selectNodes(selectedNodes)
}

View File

@@ -0,0 +1,356 @@
/**
* Tom Select v2.0.0-rc.4
* Licensed under the Apache License, Version 2.0 (the "License");
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).TomSelect=t()}(this,(function(){"use strict"
function e(e,t){e.split(/\s+/).forEach((e=>{t(e)}))}class t{constructor(){this._events={}}on(t,i){e(t,(e=>{this._events[e]=this._events[e]||[],this._events[e].push(i)}))}off(t,i){var s=arguments.length
0!==s?e(t,(e=>{if(1===s)return delete this._events[e]
e in this._events!=!1&&this._events[e].splice(this._events[e].indexOf(i),1)})):this._events={}}trigger(t,...i){var s=this
e(t,(e=>{if(e in s._events!=!1)for(let t of s._events[e])t.apply(s,i)}))}}var i
const s="[̀-ͯ·ʾ]",n=new RegExp(s,"g")
var o
const r={"æ":"ae","ⱥ":"a","ø":"o"},l=new RegExp(Object.keys(r).join("|"),"g"),a=[[67,67],[160,160],[192,438],[452,652],[961,961],[1019,1019],[1083,1083],[1281,1289],[1984,1984],[5095,5095],[7429,7441],[7545,7549],[7680,7935],[8580,8580],[9398,9449],[11360,11391],[42792,42793],[42802,42851],[42873,42897],[42912,42922],[64256,64260],[65313,65338],[65345,65370]],c=e=>e.normalize("NFKD").replace(n,"").toLowerCase().replace(l,(function(e){return r[e]})),d=(e,t="|")=>{if(1==e.length)return e[0]
var i=1
return e.forEach((e=>{i=Math.max(i,e.length)})),1==i?"["+e.join("")+"]":"(?:"+e.join(t)+")"},p=e=>{if(1===e.length)return[[e]]
var t=[]
return p(e.substring(1)).forEach((function(i){var s=i.slice(0)
s[0]=e.charAt(0)+s[0],t.push(s),(s=i.slice(0)).unshift(e.charAt(0)),t.push(s)})),t},u=e=>{void 0===o&&(o=(()=>{var e={}
a.forEach((t=>{for(let s=t[0];s<=t[1];s++){let t=String.fromCharCode(s),n=c(t)
if(n!=t.toLowerCase()){n in e||(e[n]=[n])
var i=new RegExp(d(e[n]),"iu")
t.match(i)||e[n].push(t)}}}))
var t=Object.keys(e)
t=t.sort(((e,t)=>t.length-e.length)),i=new RegExp("("+d(t)+"[̀-ͯ·ʾ]*)","g")
var s={}
return t.sort(((e,t)=>e.length-t.length)).forEach((t=>{var i=p(t).map((t=>(t=t.map((t=>e.hasOwnProperty(t)?d(e[t]):t)),d(t,""))))
s[t]=d(i)})),s})())
return e.normalize("NFKD").toLowerCase().split(i).map((e=>{if(""==e)return""
const t=c(e)
if(o.hasOwnProperty(t))return o[t]
const i=e.normalize("NFC")
return i!=e?d([e,i]):e})).join("")},h=(e,t)=>{if(e)return e[t]},g=(e,t)=>{if(e){for(var i,s=t.split(".");(i=s.shift())&&(e=e[i]););return e}},f=(e,t,i)=>{var s,n
return e?-1===(n=(e+="").search(t.regex))?0:(s=t.string.length/e.length,0===n&&(s+=.5),s*i):0},v=e=>(e+"").replace(/([\$\(-\+\.\?\[-\^\{-\}])/g,"\\$1"),m=(e,t)=>{var i=e[t]
if("function"==typeof i)return i
i&&!Array.isArray(i)&&(e[t]=[i])},y=(e,t)=>{if(Array.isArray(e))e.forEach(t)
else for(var i in e)e.hasOwnProperty(i)&&t(e[i],i)},O=(e,t)=>"number"==typeof e&&"number"==typeof t?e>t?1:e<t?-1:0:(e=c(e+"").toLowerCase())>(t=c(t+"").toLowerCase())?1:t>e?-1:0
class b{constructor(e,t){this.items=e,this.settings=t||{diacritics:!0}}tokenize(e,t,i){if(!e||!e.length)return[]
const s=[],n=e.split(/\s+/)
var o
return i&&(o=new RegExp("^("+Object.keys(i).map(v).join("|")+"):(.*)$")),n.forEach((e=>{let i,n=null,r=null
o&&(i=e.match(o))&&(n=i[1],e=i[2]),e.length>0&&(r=v(e),this.settings.diacritics&&(r=u(r)),t&&(r="\\b"+r)),s.push({string:e,regex:r?new RegExp(r,"iu"):null,field:n})})),s}getScoreFunction(e,t){var i=this.prepareSearch(e,t)
return this._getScoreFunction(i)}_getScoreFunction(e){const t=e.tokens,i=t.length
if(!i)return function(){return 0}
const s=e.options.fields,n=e.weights,o=s.length,r=e.getAttrFn
if(!o)return function(){return 1}
const l=1===o?function(e,t){const i=s[0].field
return f(r(t,i),e,n[i])}:function(e,t){var i=0
if(e.field){const s=r(t,e.field)
!e.regex&&s?i+=1/o:i+=f(s,e,1)}else y(n,((s,n)=>{i+=f(r(t,n),e,s)}))
return i/o}
return 1===i?function(e){return l(t[0],e)}:"and"===e.options.conjunction?function(e){for(var s,n=0,o=0;n<i;n++){if((s=l(t[n],e))<=0)return 0
o+=s}return o/i}:function(e){var s=0
return y(t,(t=>{s+=l(t,e)})),s/i}}getSortFunction(e,t){var i=this.prepareSearch(e,t)
return this._getSortFunction(i)}_getSortFunction(e){var t,i,s
const n=this,o=e.options,r=!e.query&&o.sort_empty?o.sort_empty:o.sort,l=[],a=[]
if("function"==typeof r)return r.bind(this)
const c=function(t,i){return"$score"===t?i.score:e.getAttrFn(n.items[i.id],t)}
if(r)for(t=0,i=r.length;t<i;t++)(e.query||"$score"!==r[t].field)&&l.push(r[t])
if(e.query){for(s=!0,t=0,i=l.length;t<i;t++)if("$score"===l[t].field){s=!1
break}s&&l.unshift({field:"$score",direction:"desc"})}else for(t=0,i=l.length;t<i;t++)if("$score"===l[t].field){l.splice(t,1)
break}for(t=0,i=l.length;t<i;t++)a.push("desc"===l[t].direction?-1:1)
const d=l.length
if(d){if(1===d){const e=l[0].field,t=a[0]
return function(i,s){return t*O(c(e,i),c(e,s))}}return function(e,t){var i,s,n
for(i=0;i<d;i++)if(n=l[i].field,s=a[i]*O(c(n,e),c(n,t)))return s
return 0}}return null}prepareSearch(e,t){const i={}
var s=Object.assign({},t)
if(m(s,"sort"),m(s,"sort_empty"),s.fields){m(s,"fields")
const e=[]
s.fields.forEach((t=>{"string"==typeof t&&(t={field:t,weight:1}),e.push(t),i[t.field]="weight"in t?t.weight:1})),s.fields=e}return{options:s,query:e.toLowerCase().trim(),tokens:this.tokenize(e,s.respect_word_boundaries,i),total:0,items:[],weights:i,getAttrFn:s.nesting?g:h}}search(e,t){var i,s,n=this
s=this.prepareSearch(e,t),t=s.options,e=s.query
const o=t.score||n._getScoreFunction(s)
e.length?y(n.items,((e,n)=>{i=o(e),(!1===t.filter||i>0)&&s.items.push({score:i,id:n})})):y(n.items,((e,t)=>{s.items.push({score:1,id:t})}))
const r=n._getSortFunction(s)
return r&&s.items.sort(r),s.total=s.items.length,"number"==typeof t.limit&&(s.items=s.items.slice(0,t.limit)),s}}const w=e=>{if(e.jquery)return e[0]
if(e instanceof HTMLElement)return e
if(e.indexOf("<")>-1){let t=document.createElement("div")
return t.innerHTML=e.trim(),t.firstChild}return document.querySelector(e)},_=(e,t)=>{var i=document.createEvent("HTMLEvents")
i.initEvent(t,!0,!1),e.dispatchEvent(i)},I=(e,t)=>{Object.assign(e.style,t)},C=(e,...t)=>{var i=A(t);(e=x(e)).map((e=>{i.map((t=>{e.classList.add(t)}))}))},S=(e,...t)=>{var i=A(t);(e=x(e)).map((e=>{i.map((t=>{e.classList.remove(t)}))}))},A=e=>{var t=[]
return y(e,(e=>{"string"==typeof e&&(e=e.trim().split(/[\11\12\14\15\40]/)),Array.isArray(e)&&(t=t.concat(e))})),t.filter(Boolean)},x=e=>(Array.isArray(e)||(e=[e]),e),k=(e,t,i)=>{if(!i||i.contains(e))for(;e&&e.matches;){if(e.matches(t))return e
e=e.parentNode}},F=(e,t=0)=>t>0?e[e.length-1]:e[0],L=(e,t)=>{if(!e)return-1
t=t||e.nodeName
for(var i=0;e=e.previousElementSibling;)e.matches(t)&&i++
return i},P=(e,t)=>{y(t,((t,i)=>{null==t?e.removeAttribute(i):e.setAttribute(i,""+t)}))},E=(e,t)=>{e.parentNode&&e.parentNode.replaceChild(t,e)},T=(e,t)=>{if(null===t)return
if("string"==typeof t){if(!t.length)return
t=new RegExp(t,"i")}const i=e=>3===e.nodeType?(e=>{var i=e.data.match(t)
if(i&&e.data.length>0){var s=document.createElement("span")
s.className="highlight"
var n=e.splitText(i.index)
n.splitText(i[0].length)
var o=n.cloneNode(!0)
return s.appendChild(o),E(n,s),1}return 0})(e):((e=>{if(1===e.nodeType&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&("highlight"!==e.className||"SPAN"!==e.tagName))for(var t=0;t<e.childNodes.length;++t)t+=i(e.childNodes[t])})(e),0)
i(e)},V="undefined"!=typeof navigator&&/Mac/.test(navigator.userAgent)?"metaKey":"ctrlKey"
var j={options:[],optgroups:[],plugins:[],delimiter:",",splitOn:null,persist:!0,diacritics:!0,create:null,createOnBlur:!1,createFilter:null,highlight:!0,openOnFocus:!0,shouldOpen:null,maxOptions:50,maxItems:null,hideSelected:null,duplicates:!1,addPrecedence:!1,selectOnTab:!1,preload:null,allowEmptyOption:!1,loadThrottle:300,loadingClass:"loading",dataAttr:null,optgroupField:"optgroup",valueField:"value",labelField:"text",disabledField:"disabled",optgroupLabelField:"label",optgroupValueField:"value",lockOptgroupOrder:!1,sortField:"$order",searchField:["text"],searchConjunction:"and",mode:null,wrapperClass:"ts-wrapper",controlClass:"ts-control",dropdownClass:"ts-dropdown",dropdownContentClass:"ts-dropdown-content",itemClass:"item",optionClass:"option",dropdownParent:null,copyClassesToDropdown:!1,placeholder:null,hidePlaceholder:null,shouldLoad:function(e){return e.length>0},render:{}}
const q=e=>null==e?null:D(e),D=e=>"boolean"==typeof e?e?"1":"0":e+"",N=e=>(e+"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),z=(e,t)=>{var i
return function(s,n){var o=this
i&&(o.loading=Math.max(o.loading-1,0),clearTimeout(i)),i=setTimeout((function(){i=null,o.loadedSearches[s]=!0,e.call(o,s,n)}),t)}},R=(e,t,i)=>{var s,n=e.trigger,o={}
for(s in e.trigger=function(){var i=arguments[0]
if(-1===t.indexOf(i))return n.apply(e,arguments)
o[i]=arguments},i.apply(e,[]),e.trigger=n,o)n.apply(e,o[s])},H=(e,t=!1)=>{e&&(e.preventDefault(),t&&e.stopPropagation())},B=(e,t,i,s)=>{e.addEventListener(t,i,s)},K=(e,t)=>!!t&&(!!t[e]&&1===(t.altKey?1:0)+(t.ctrlKey?1:0)+(t.shiftKey?1:0)+(t.metaKey?1:0)),M=(e,t)=>{const i=e.getAttribute("id")
return i||(e.setAttribute("id",t),t)},Q=e=>e.replace(/[\\"']/g,"\\$&"),G=(e,t)=>{t&&e.append(t)}
function U(e,t){var i=Object.assign({},j,t),s=i.dataAttr,n=i.labelField,o=i.valueField,r=i.disabledField,l=i.optgroupField,a=i.optgroupLabelField,c=i.optgroupValueField,d=e.tagName.toLowerCase(),p=e.getAttribute("placeholder")||e.getAttribute("data-placeholder")
if(!p&&!i.allowEmptyOption){let t=e.querySelector('option[value=""]')
t&&(p=t.textContent)}var u,h,g,f,v,m,O={placeholder:p,options:[],optgroups:[],items:[],maxItems:null}
return"select"===d?(h=O.options,g={},f=1,v=e=>{var t=Object.assign({},e.dataset),i=s&&t[s]
return"string"==typeof i&&i.length&&(t=Object.assign(t,JSON.parse(i))),t},m=(e,t)=>{var s=q(e.value)
if(null!=s&&(s||i.allowEmptyOption)){if(g.hasOwnProperty(s)){if(t){var a=g[s][l]
a?Array.isArray(a)?a.push(t):g[s][l]=[a,t]:g[s][l]=t}}else{var c=v(e)
c[n]=c[n]||e.textContent,c[o]=c[o]||s,c[r]=c[r]||e.disabled,c[l]=c[l]||t,c.$option=e,g[s]=c,h.push(c)}e.selected&&O.items.push(s)}},O.maxItems=e.hasAttribute("multiple")?null:1,y(e.children,(e=>{var t,i,s
"optgroup"===(u=e.tagName.toLowerCase())?((s=v(t=e))[a]=s[a]||t.getAttribute("label")||"",s[c]=s[c]||f++,s[r]=s[r]||t.disabled,O.optgroups.push(s),i=s[c],y(t.children,(e=>{m(e,i)}))):"option"===u&&m(e)}))):(()=>{const t=e.getAttribute(s)
if(t)O.options=JSON.parse(t),y(O.options,(e=>{O.items.push(e[o])}))
else{var r=e.value.trim()||""
if(!i.allowEmptyOption&&!r.length)return
const t=r.split(i.delimiter)
y(t,(e=>{const t={}
t[n]=e,t[o]=e,O.options.push(t)})),O.items=t}})(),Object.assign({},j,O,t)}var W=0
class J extends(function(e){return e.plugins={},class extends e{constructor(...e){super(...e),this.plugins={names:[],settings:{},requested:{},loaded:{}}}static define(t,i){e.plugins[t]={name:t,fn:i}}initializePlugins(e){var t,i
const s=this,n=[]
if(Array.isArray(e))e.forEach((e=>{"string"==typeof e?n.push(e):(s.plugins.settings[e.name]=e.options,n.push(e.name))}))
else if(e)for(t in e)e.hasOwnProperty(t)&&(s.plugins.settings[t]=e[t],n.push(t))
for(;i=n.shift();)s.require(i)}loadPlugin(t){var i=this,s=i.plugins,n=e.plugins[t]
if(!e.plugins.hasOwnProperty(t))throw new Error('Unable to find "'+t+'" plugin')
s.requested[t]=!0,s.loaded[t]=n.fn.apply(i,[i.plugins.settings[t]||{}]),s.names.push(t)}require(e){var t=this,i=t.plugins
if(!t.plugins.loaded.hasOwnProperty(e)){if(i.requested[e])throw new Error('Plugin has circular dependency ("'+e+'")')
t.loadPlugin(e)}return i.loaded[e]}}}(t)){constructor(e,t){var i
super(),this.order=0,this.isOpen=!1,this.isDisabled=!1,this.isInvalid=!1,this.isValid=!0,this.isLocked=!1,this.isFocused=!1,this.isInputHidden=!1,this.isSetup=!1,this.ignoreFocus=!1,this.hasOptions=!1,this.lastValue="",this.caretPos=0,this.loading=0,this.loadedSearches={},this.activeOption=null,this.activeItems=[],this.optgroups={},this.options={},this.userOptions={},this.items=[],W++
var s=w(e)
if(s.tomselect)throw new Error("Tom Select already initialized on this element")
s.tomselect=this,i=(window.getComputedStyle&&window.getComputedStyle(s,null)).getPropertyValue("direction")
const n=U(s,t)
this.settings=n,this.input=s,this.tabIndex=s.tabIndex||0,this.is_select_tag="select"===s.tagName.toLowerCase(),this.rtl=/rtl/i.test(i),this.inputId=M(s,"tomselect-"+W),this.isRequired=s.required,this.sifter=new b(this.options,{diacritics:n.diacritics}),n.mode=n.mode||(1===n.maxItems?"single":"multi"),"boolean"!=typeof n.hideSelected&&(n.hideSelected="multi"===n.mode),"boolean"!=typeof n.hidePlaceholder&&(n.hidePlaceholder="multi"!==n.mode)
var o=n.createFilter
"function"!=typeof o&&("string"==typeof o&&(o=new RegExp(o)),o instanceof RegExp?n.createFilter=e=>o.test(e):n.createFilter=()=>!0),this.initializePlugins(n.plugins),this.setupCallbacks(),this.setupTemplates()
const r=w("<div>"),l=w("<div>"),a=this._render("dropdown"),c=w('<div role="listbox" tabindex="-1">'),d=this.input.getAttribute("class")||"",p=n.mode
var u
if(C(r,n.wrapperClass,d,p),C(l,n.controlClass),G(r,l),C(a,n.dropdownClass,p),n.copyClassesToDropdown&&C(a,d),C(c,n.dropdownContentClass),G(a,c),w(n.dropdownParent||r).appendChild(a),n.hasOwnProperty("controlInput"))n.controlInput?(u=w(n.controlInput),this.focus_node=u):(u=w("<input/>"),this.focus_node=l)
else{u=w('<input type="text" autocomplete="off" size="1" />')
y(["autocorrect","autocapitalize","autocomplete"],(e=>{s.getAttribute(e)&&P(u,{[e]:s.getAttribute(e)})})),u.tabIndex=-1,l.appendChild(u),this.focus_node=u}this.wrapper=r,this.dropdown=a,this.dropdown_content=c,this.control=l,this.control_input=u,this.setup()}setup(){const e=this,t=e.settings,i=e.control_input,s=e.dropdown,n=e.dropdown_content,o=e.wrapper,r=e.control,l=e.input,a=e.focus_node,c={passive:!0},d=e.inputId+"-ts-dropdown"
P(n,{id:d}),P(a,{role:"combobox","aria-haspopup":"listbox","aria-expanded":"false","aria-controls":d})
const p=M(a,e.inputId+"-ts-control"),u="label[for='"+(e=>e.replace(/['"\\]/g,"\\$&"))(e.inputId)+"']",h=document.querySelector(u),g=e.focus.bind(e)
if(h){B(h,"click",g),P(h,{for:p})
const t=M(h,e.inputId+"-ts-label")
P(a,{"aria-labelledby":t}),P(n,{"aria-labelledby":t})}if(o.style.width=l.style.width,e.plugins.names.length){const t="plugin-"+e.plugins.names.join(" plugin-")
C([o,s],t)}(null===t.maxItems||t.maxItems>1)&&e.is_select_tag&&P(l,{multiple:"multiple"}),e.settings.placeholder&&P(i,{placeholder:t.placeholder}),!e.settings.splitOn&&e.settings.delimiter&&(e.settings.splitOn=new RegExp("\\s*"+v(e.settings.delimiter)+"+\\s*")),t.load&&t.loadThrottle&&(t.load=z(t.load,t.loadThrottle)),e.control_input.type=l.type,B(s,"click",(t=>{const i=k(t.target,"[data-selectable]")
i&&(e.onOptionSelect(t,i),H(t,!0))})),B(r,"click",(t=>{var s=k(t.target,"[data-ts-item]",r)
s&&e.onItemSelect(t,s)?H(t,!0):""==i.value&&(e.onClick(),H(t,!0))})),B(i,"mousedown",(e=>{""!==i.value&&e.stopPropagation()})),B(a,"keydown",(t=>e.onKeyDown(t))),B(i,"keypress",(t=>e.onKeyPress(t))),B(i,"input",(t=>e.onInput(t))),B(a,"resize",(()=>e.positionDropdown()),c),B(a,"blur",(t=>e.onBlur(t))),B(a,"focus",(t=>e.onFocus(t))),B(a,"paste",(t=>e.onPaste(t)))
const f=t=>{const i=t.composedPath()[0]
if(!o.contains(i)&&!s.contains(i))return e.isFocused&&e.blur(),void e.inputState()
H(t,!0)}
var m=()=>{e.isOpen&&e.positionDropdown()}
B(document,"mousedown",f),B(window,"scroll",m,c),B(window,"resize",m,c),this._destroy=()=>{document.removeEventListener("mousedown",f),window.removeEventListener("sroll",m),window.removeEventListener("resize",m),h&&h.removeEventListener("click",g)},this.revertSettings={innerHTML:l.innerHTML,tabIndex:l.tabIndex},l.tabIndex=-1,l.insertAdjacentElement("afterend",e.wrapper),e.sync(!1),t.items=[],delete t.optgroups,delete t.options,B(l,"invalid",(t=>{e.isValid&&(e.isValid=!1,e.isInvalid=!0,e.refreshState())})),e.updateOriginalInput(),e.refreshItems(),e.close(!1),e.inputState(),e.isSetup=!0,l.disabled?e.disable():e.enable(),e.on("change",this.onChange),C(l,"tomselected","ts-hidden-accessible"),e.trigger("initialize"),!0===t.preload&&e.preload()}setupOptions(e=[],t=[]){this.addOptions(e),y(t,(e=>{this.registerOptionGroup(e)}))}setupTemplates(){var e=this,t=e.settings.labelField,i=e.settings.optgroupLabelField,s={optgroup:e=>{let t=document.createElement("div")
return t.className="optgroup",t.appendChild(e.options),t},optgroup_header:(e,t)=>'<div class="optgroup-header">'+t(e[i])+"</div>",option:(e,i)=>"<div>"+i(e[t])+"</div>",item:(e,i)=>"<div>"+i(e[t])+"</div>",option_create:(e,t)=>'<div class="create">Add <strong>'+t(e.input)+"</strong>&hellip;</div>",no_results:()=>'<div class="no-results">No results found</div>',loading:()=>'<div class="spinner"></div>',not_loading:()=>{},dropdown:()=>"<div></div>"}
e.settings.render=Object.assign({},s,e.settings.render)}setupCallbacks(){var e,t,i={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",item_select:"onItemSelect",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"}
for(e in i)(t=this.settings[i[e]])&&this.on(e,t)}sync(e=!0){const t=this,i=e?U(t.input,{delimiter:t.settings.delimiter}):t.settings
t.setupOptions(i.options,i.optgroups),t.setValue(i.items,!0),t.lastQuery=null}onClick(){var e=this
if(e.activeItems.length>0)return e.clearActiveItems(),void e.focus()
e.isFocused&&e.isOpen?e.blur():e.focus()}onMouseDown(){}onChange(){_(this.input,"input"),_(this.input,"change")}onPaste(e){var t=this
t.isFull()||t.isInputHidden||t.isLocked?H(e):t.settings.splitOn&&setTimeout((()=>{var e=t.inputValue()
if(e.match(t.settings.splitOn)){var i=e.trim().split(t.settings.splitOn)
y(i,(e=>{t.createItem(e)}))}}),0)}onKeyPress(e){var t=this
if(!t.isLocked){var i=String.fromCharCode(e.keyCode||e.which)
return t.settings.create&&"multi"===t.settings.mode&&i===t.settings.delimiter?(t.createItem(),void H(e)):void 0}H(e)}onKeyDown(e){var t=this
if(t.isLocked)9!==e.keyCode&&H(e)
else{switch(e.keyCode){case 65:if(K(V,e))return H(e),void t.selectAll()
break
case 27:return t.isOpen&&(H(e,!0),t.close()),void t.clearActiveItems()
case 40:if(!t.isOpen&&t.hasOptions)t.open()
else if(t.activeOption){let e=t.getAdjacent(t.activeOption,1)
e&&t.setActiveOption(e)}return void H(e)
case 38:if(t.activeOption){let e=t.getAdjacent(t.activeOption,-1)
e&&t.setActiveOption(e)}return void H(e)
case 13:return void(t.isOpen&&t.activeOption?(t.onOptionSelect(e,t.activeOption),H(e)):t.settings.create&&t.createItem()&&H(e))
case 37:return void t.advanceSelection(-1,e)
case 39:return void t.advanceSelection(1,e)
case 9:return void(t.settings.selectOnTab&&(t.isOpen&&t.activeOption&&(t.onOptionSelect(e,t.activeOption),H(e)),t.settings.create&&t.createItem()&&H(e)))
case 8:case 46:return void t.deleteSelection(e)}t.isInputHidden&&!K(V,e)&&H(e)}}onInput(e){var t=this
if(!t.isLocked){var i=t.inputValue()
t.lastValue!==i&&(t.lastValue=i,t.settings.shouldLoad.call(t,i)&&t.load(i),t.refreshOptions(),t.trigger("type",i))}}onFocus(e){var t=this,i=t.isFocused
if(t.isDisabled)return t.blur(),void H(e)
t.ignoreFocus||(t.isFocused=!0,"focus"===t.settings.preload&&t.preload(),i||t.trigger("focus"),t.activeItems.length||(t.showInput(),t.refreshOptions(!!t.settings.openOnFocus)),t.refreshState())}onBlur(e){if(!1!==document.hasFocus()){var t=this
if(t.isFocused){t.isFocused=!1,t.ignoreFocus=!1
var i=()=>{t.close(),t.setActiveItem(),t.setCaret(t.items.length),t.trigger("blur")}
t.settings.create&&t.settings.createOnBlur?t.createItem(null,!1,i):i()}}}onOptionSelect(e,t){var i,s=this
t&&(t.parentElement&&t.parentElement.matches("[data-disabled]")||(t.classList.contains("create")?s.createItem(null,!0,(()=>{s.settings.closeAfterSelect&&s.close()})):void 0!==(i=t.dataset.value)&&(s.lastQuery=null,s.addItem(i),s.settings.closeAfterSelect&&s.close(),!s.settings.hideSelected&&e.type&&/click/.test(e.type)&&s.setActiveOption(t))))}onItemSelect(e,t){var i=this
return!i.isLocked&&"multi"===i.settings.mode&&(H(e),i.setActiveItem(t,e),!0)}canLoad(e){return!!this.settings.load&&!this.loadedSearches.hasOwnProperty(e)}load(e){const t=this
if(!t.canLoad(e))return
C(t.wrapper,t.settings.loadingClass),t.loading++
const i=t.loadCallback.bind(t)
t.settings.load.call(t,e,i)}loadCallback(e,t){const i=this
i.loading=Math.max(i.loading-1,0),i.lastQuery=null,i.clearActiveOption(),i.setupOptions(e,t),i.refreshOptions(i.isFocused&&!i.isInputHidden),i.loading||S(i.wrapper,i.settings.loadingClass),i.trigger("load",e,t)}preload(){var e=this.wrapper.classList
e.contains("preloaded")||(e.add("preloaded"),this.load(""))}setTextboxValue(e=""){var t=this.control_input
t.value!==e&&(t.value=e,_(t,"update"),this.lastValue=e)}getValue(){return this.is_select_tag&&this.input.hasAttribute("multiple")?this.items:this.items.join(this.settings.delimiter)}setValue(e,t){R(this,t?[]:["change"],(()=>{this.clear(t),this.addItems(e,t)}))}setMaxItems(e){0===e&&(e=null),this.settings.maxItems=e,this.refreshState()}setActiveItem(e,t){var i,s,n,o,r,l,a=this
if("single"!==a.settings.mode){if(!e)return a.clearActiveItems(),void(a.isFocused&&a.showInput())
if("click"===(i=t&&t.type.toLowerCase())&&K("shiftKey",t)&&a.activeItems.length){for(l=a.getLastActive(),(n=Array.prototype.indexOf.call(a.control.children,l))>(o=Array.prototype.indexOf.call(a.control.children,e))&&(r=n,n=o,o=r),s=n;s<=o;s++)e=a.control.children[s],-1===a.activeItems.indexOf(e)&&a.setActiveItemClass(e)
H(t)}else"click"===i&&K(V,t)||"keydown"===i&&K("shiftKey",t)?e.classList.contains("active")?a.removeActiveItem(e):a.setActiveItemClass(e):(a.clearActiveItems(),a.setActiveItemClass(e))
a.hideInput(),a.isFocused||a.focus()}}setActiveItemClass(e){const t=this,i=t.control.querySelector(".last-active")
i&&S(i,"last-active"),C(e,"active last-active"),t.trigger("item_select",e),-1==t.activeItems.indexOf(e)&&t.activeItems.push(e)}removeActiveItem(e){var t=this.activeItems.indexOf(e)
this.activeItems.splice(t,1),S(e,"active")}clearActiveItems(){S(this.activeItems,"active"),this.activeItems=[]}setActiveOption(e){e!==this.activeOption&&(this.clearActiveOption(),e&&(this.activeOption=e,P(this.focus_node,{"aria-activedescendant":e.getAttribute("id")}),P(e,{"aria-selected":"true"}),C(e,"active"),this.scrollToOption(e)))}scrollToOption(e,t){if(!e)return
const i=this.dropdown_content,s=i.clientHeight,n=i.scrollTop||0,o=e.offsetHeight,r=e.getBoundingClientRect().top-i.getBoundingClientRect().top+n
r+o>s+n?this.scroll(r-s+o,t):r<n&&this.scroll(r,t)}scroll(e,t){const i=this.dropdown_content
t&&(i.style.scrollBehavior=t),i.scrollTop=e,i.style.scrollBehavior=""}clearActiveOption(){this.activeOption&&(S(this.activeOption,"active"),P(this.activeOption,{"aria-selected":null})),this.activeOption=null,P(this.focus_node,{"aria-activedescendant":null})}selectAll(){if("single"===this.settings.mode)return
const e=this.controlChildren()
e.length&&(this.hideInput(),this.close(),this.activeItems=e,C(e,"active"))}inputState(){var e=this
e.control.contains(e.control_input)&&(P(e.control_input,{placeholder:e.settings.placeholder}),e.activeItems.length>0||!e.isFocused&&e.settings.hidePlaceholder&&e.items.length>0?(e.setTextboxValue(),e.isInputHidden=!0):(e.settings.hidePlaceholder&&e.items.length>0&&P(e.control_input,{placeholder:""}),e.isInputHidden=!1),e.wrapper.classList.toggle("input-hidden",e.isInputHidden))}hideInput(){this.inputState()}showInput(){this.inputState()}inputValue(){return this.control_input.value.trim()}focus(){var e=this
e.isDisabled||(e.ignoreFocus=!0,e.control_input.offsetWidth?e.control_input.focus():e.focus_node.focus(),setTimeout((()=>{e.ignoreFocus=!1,e.onFocus()}),0))}blur(){this.focus_node.blur(),this.onBlur()}getScoreFunction(e){return this.sifter.getScoreFunction(e,this.getSearchOptions())}getSearchOptions(){var e=this.settings,t=e.sortField
return"string"==typeof e.sortField&&(t=[{field:e.sortField}]),{fields:e.searchField,conjunction:e.searchConjunction,sort:t,nesting:e.nesting}}search(e){var t,i,s,n=this,o=this.getSearchOptions()
if(n.settings.score&&"function"!=typeof(s=n.settings.score.call(n,e)))throw new Error('Tom Select "score" setting must be a function that returns a function')
if(e!==n.lastQuery?(n.lastQuery=e,i=n.sifter.search(e,Object.assign(o,{score:s})),n.currentResults=i):i=Object.assign({},n.currentResults),n.settings.hideSelected)for(t=i.items.length-1;t>=0;t--){let e=q(i.items[t].id)
e&&-1!==n.items.indexOf(e)&&i.items.splice(t,1)}return i}refreshOptions(e=!0){var t,i,s,n,o,r,l,a,c,d,p
const u={},h=[]
var g,f=this,v=f.inputValue(),m=f.search(v),O=f.activeOption,b=f.settings.shouldOpen||!1,w=f.dropdown_content
for(O&&(c=O.dataset.value,d=O.closest("[data-group]")),n=m.items.length,"number"==typeof f.settings.maxOptions&&(n=Math.min(n,f.settings.maxOptions)),n>0&&(b=!0),t=0;t<n;t++){let e=m.items[t].id,n=f.options[e],l=f.getOption(e,!0)
for(f.settings.hideSelected||l.classList.toggle("selected",f.items.includes(e)),o=n[f.settings.optgroupField]||"",i=0,s=(r=Array.isArray(o)?o:[o])&&r.length;i<s;i++)o=r[i],f.optgroups.hasOwnProperty(o)||(o=""),u.hasOwnProperty(o)||(u[o]=document.createDocumentFragment(),h.push(o)),i>0&&(l=l.cloneNode(!0),P(l,{id:n.$id+"-clone-"+i,"aria-selected":null}),l.classList.add("ts-cloned"),S(l,"active")),c==e&&d&&d.dataset.group===o&&(O=l),u[o].appendChild(l)}this.settings.lockOptgroupOrder&&h.sort(((e,t)=>(f.optgroups[e]&&f.optgroups[e].$order||0)-(f.optgroups[t]&&f.optgroups[t].$order||0))),l=document.createDocumentFragment(),y(h,(e=>{if(f.optgroups.hasOwnProperty(e)&&u[e].children.length){let t=document.createDocumentFragment(),i=f.render("optgroup_header",f.optgroups[e])
G(t,i),G(t,u[e])
let s=f.render("optgroup",{group:f.optgroups[e],options:t})
G(l,s)}else G(l,u[e])})),w.innerHTML="",G(w,l),f.settings.highlight&&(g=w.querySelectorAll("span.highlight"),Array.prototype.forEach.call(g,(function(e){var t=e.parentNode
t.replaceChild(e.firstChild,e),t.normalize()})),m.query.length&&m.tokens.length&&y(m.tokens,(e=>{T(w,e.regex)})))
var _=e=>{let t=f.render(e,{input:v})
return t&&(b=!0,w.insertBefore(t,w.firstChild)),t}
if(f.loading?_("loading"):f.settings.shouldLoad.call(f,v)?0===m.items.length&&_("no_results"):_("not_loading"),(a=f.canCreate(v))&&(p=_("option_create")),f.hasOptions=m.items.length>0||a,b){if(m.items.length>0){if(!w.contains(O)&&"single"===f.settings.mode&&f.items.length&&(O=f.getOption(f.items[0])),!w.contains(O)){let e=0
p&&!f.settings.addPrecedence&&(e=1),O=f.selectable()[e]}}else p&&(O=p)
e&&!f.isOpen&&(f.open(),f.scrollToOption(O,"auto")),f.setActiveOption(O)}else f.clearActiveOption(),e&&f.isOpen&&f.close(!1)}selectable(){return this.dropdown_content.querySelectorAll("[data-selectable]")}addOption(e,t=!1){const i=this
if(Array.isArray(e))return i.addOptions(e,t),!1
const s=q(e[i.settings.valueField])
return null!==s&&!i.options.hasOwnProperty(s)&&(e.$order=e.$order||++i.order,e.$id=i.inputId+"-opt-"+e.$order,i.options[s]=e,i.lastQuery=null,t&&(i.userOptions[s]=t,i.trigger("option_add",s,e)),s)}addOptions(e,t=!1){y(e,(e=>{this.addOption(e,t)}))}registerOption(e){return this.addOption(e)}registerOptionGroup(e){var t=q(e[this.settings.optgroupValueField])
return null!==t&&(e.$order=e.$order||++this.order,this.optgroups[t]=e,t)}addOptionGroup(e,t){var i
t[this.settings.optgroupValueField]=e,(i=this.registerOptionGroup(t))&&this.trigger("optgroup_add",i,t)}removeOptionGroup(e){this.optgroups.hasOwnProperty(e)&&(delete this.optgroups[e],this.clearCache(),this.trigger("optgroup_remove",e))}clearOptionGroups(){this.optgroups={},this.clearCache(),this.trigger("optgroup_clear")}updateOption(e,t){const i=this
var s,n
const o=q(e),r=q(t[i.settings.valueField])
if(null===o)return
if(!i.options.hasOwnProperty(o))return
if("string"!=typeof r)throw new Error("Value must be set in option data")
const l=i.getOption(o),a=i.getItem(o)
if(t.$order=t.$order||i.options[o].$order,delete i.options[o],i.uncacheValue(r),i.options[r]=t,l){if(i.dropdown_content.contains(l)){const e=i._render("option",t)
E(l,e),i.activeOption===l&&i.setActiveOption(e)}l.remove()}a&&(-1!==(n=i.items.indexOf(o))&&i.items.splice(n,1,r),s=i._render("item",t),a.classList.contains("active")&&C(s,"active"),E(a,s)),i.lastQuery=null}removeOption(e,t){const i=this
e=D(e),i.uncacheValue(e),delete i.userOptions[e],delete i.options[e],i.lastQuery=null,i.trigger("option_remove",e),i.removeItem(e,t)}clearOptions(){this.loadedSearches={},this.userOptions={},this.clearCache()
var e={}
y(this.options,((t,i)=>{this.items.indexOf(i)>=0&&(e[i]=this.options[i])})),this.options=this.sifter.items=e,this.lastQuery=null,this.trigger("option_clear")}getOption(e,t=!1){const i=q(e)
if(null!==i&&this.options.hasOwnProperty(i)){const e=this.options[i]
if(e.$div)return e.$div
if(t)return this._render("option",e)}return null}getAdjacent(e,t,i="option"){var s
if(!e)return null
s="item"==i?this.controlChildren():this.dropdown_content.querySelectorAll("[data-selectable]")
for(let i=0;i<s.length;i++)if(s[i]==e)return t>0?s[i+1]:s[i-1]
return null}getItem(e){if("object"==typeof e)return e
var t=q(e)
return null!==t?this.control.querySelector(`[data-value="${Q(t)}"]`):null}addItems(e,t){var i=this,s=Array.isArray(e)?e:[e]
for(let e=0,n=(s=s.filter((e=>-1===i.items.indexOf(e)))).length;e<n;e++)i.isPending=e<n-1,i.addItem(s[e],t)}addItem(e,t){R(this,t?[]:["change"],(()=>{var i,s
const n=this,o=n.settings.mode,r=q(e)
if((!r||-1===n.items.indexOf(r)||("single"===o&&n.close(),"single"!==o&&n.settings.duplicates))&&null!==r&&n.options.hasOwnProperty(r)&&("single"===o&&n.clear(t),"multi"!==o||!n.isFull())){if(i=n._render("item",n.options[r]),n.control.contains(i)&&(i=i.cloneNode(!0)),s=n.isFull(),n.items.splice(n.caretPos,0,r),n.insertAtCaret(i),n.isSetup){if(!n.isPending&&n.settings.hideSelected){let e=n.getOption(r),t=n.getAdjacent(e,1)
t&&n.setActiveOption(t)}n.isPending||n.refreshOptions(n.isFocused&&"single"!==o),0!=n.settings.closeAfterSelect&&n.isFull()?n.close():n.isPending||n.positionDropdown(),n.trigger("item_add",r,i),n.isPending||n.updateOriginalInput({silent:t})}(!n.isPending||!s&&n.isFull())&&(n.inputState(),n.refreshState())}}))}removeItem(e=null,t){const i=this
if(!(e=i.getItem(e)))return
var s,n
const o=e.dataset.value
s=L(e),e.remove(),e.classList.contains("active")&&(n=i.activeItems.indexOf(e),i.activeItems.splice(n,1),S(e,"active")),i.items.splice(s,1),i.lastQuery=null,!i.settings.persist&&i.userOptions.hasOwnProperty(o)&&i.removeOption(o,t),s<i.caretPos&&i.setCaret(i.caretPos-1),i.updateOriginalInput({silent:t}),i.refreshState(),i.positionDropdown(),i.trigger("item_remove",o,e)}createItem(e=null,t=!0,i=(()=>{})){var s,n=this,o=n.caretPos
if(e=e||n.inputValue(),!n.canCreate(e))return i(),!1
n.lock()
var r=!1,l=e=>{if(n.unlock(),!e||"object"!=typeof e)return i()
var s=q(e[n.settings.valueField])
if("string"!=typeof s)return i()
n.setTextboxValue(),n.addOption(e,!0),n.setCaret(o),n.addItem(s),n.refreshOptions(t&&"single"!==n.settings.mode),i(e),r=!0}
return s="function"==typeof n.settings.create?n.settings.create.call(this,e,l):{[n.settings.labelField]:e,[n.settings.valueField]:e},r||l(s),!0}refreshItems(){var e=this
e.lastQuery=null,e.isSetup&&e.addItems(e.items),e.updateOriginalInput(),e.refreshState()}refreshState(){const e=this
e.refreshValidityState()
const t=e.isFull(),i=e.isLocked
e.wrapper.classList.toggle("rtl",e.rtl)
const s=e.wrapper.classList
var n
s.toggle("focus",e.isFocused),s.toggle("disabled",e.isDisabled),s.toggle("required",e.isRequired),s.toggle("invalid",!e.isValid),s.toggle("locked",i),s.toggle("full",t),s.toggle("input-active",e.isFocused&&!e.isInputHidden),s.toggle("dropdown-active",e.isOpen),s.toggle("has-options",(n=e.options,0===Object.keys(n).length)),s.toggle("has-items",e.items.length>0)}refreshValidityState(){var e=this
e.input.checkValidity&&(e.isValid=e.input.checkValidity(),e.isInvalid=!e.isValid)}isFull(){return null!==this.settings.maxItems&&this.items.length>=this.settings.maxItems}updateOriginalInput(e={}){const t=this
var i,s
const n=t.input.querySelector('option[value=""]')
if(t.is_select_tag){const e=[]
function o(i,s,o){return i||(i=w('<option value="'+N(s)+'">'+N(o)+"</option>")),i!=n&&t.input.append(i),e.push(i),i.selected=!0,i}t.input.querySelectorAll("option:checked").forEach((e=>{e.selected=!1})),0==t.items.length&&"single"==t.settings.mode?o(n,"",""):t.items.forEach((n=>{if(i=t.options[n],s=i[t.settings.labelField]||"",e.includes(i.$option)){o(t.input.querySelector(`option[value="${Q(n)}"]:not(:checked)`),n,s)}else i.$option=o(i.$option,n,s)}))}else t.input.value=t.getValue()
t.isSetup&&(e.silent||t.trigger("change",t.getValue()))}open(){var e=this
e.isLocked||e.isOpen||"multi"===e.settings.mode&&e.isFull()||(e.isOpen=!0,P(e.focus_node,{"aria-expanded":"true"}),e.refreshState(),I(e.dropdown,{visibility:"hidden",display:"block"}),e.positionDropdown(),I(e.dropdown,{visibility:"visible",display:"block"}),e.focus(),e.trigger("dropdown_open",e.dropdown))}close(e=!0){var t=this,i=t.isOpen
e&&(t.setTextboxValue(),"single"===t.settings.mode&&t.items.length&&t.hideInput()),t.isOpen=!1,P(t.focus_node,{"aria-expanded":"false"}),I(t.dropdown,{display:"none"}),t.settings.hideSelected&&t.clearActiveOption(),t.refreshState(),i&&t.trigger("dropdown_close",t.dropdown)}positionDropdown(){if("body"===this.settings.dropdownParent){var e=this.control,t=e.getBoundingClientRect(),i=e.offsetHeight+t.top+window.scrollY,s=t.left+window.scrollX
I(this.dropdown,{width:t.width+"px",top:i+"px",left:s+"px"})}}clear(e){var t=this
if(t.items.length){var i=t.controlChildren()
y(i,(e=>{t.removeItem(e,!0)})),t.showInput(),e||t.updateOriginalInput(),t.trigger("clear")}}insertAtCaret(e){const t=this,i=t.caretPos,s=t.control
s.insertBefore(e,s.children[i]),t.setCaret(i+1)}deleteSelection(e){var t,i,s,n,o,r=this
t=e&&8===e.keyCode?-1:1,i={start:(o=r.control_input).selectionStart||0,length:(o.selectionEnd||0)-(o.selectionStart||0)}
const l=[]
if(r.activeItems.length)n=F(r.activeItems,t),s=L(n),t>0&&s++,y(r.activeItems,(e=>l.push(e)))
else if((r.isFocused||"single"===r.settings.mode)&&r.items.length){const e=r.controlChildren()
t<0&&0===i.start&&0===i.length?l.push(e[r.caretPos-1]):t>0&&i.start===r.inputValue().length&&l.push(e[r.caretPos])}const a=l.map((e=>e.dataset.value))
if(!a.length||"function"==typeof r.settings.onDelete&&!1===r.settings.onDelete.call(r,a,e))return!1
for(H(e,!0),void 0!==s&&r.setCaret(s);l.length;)r.removeItem(l.pop())
return r.showInput(),r.positionDropdown(),r.refreshOptions(!1),!0}advanceSelection(e,t){var i,s,n=this
n.rtl&&(e*=-1),n.inputValue().length||(K(V,t)||K("shiftKey",t)?(s=(i=n.getLastActive(e))?i.classList.contains("active")?n.getAdjacent(i,e,"item"):i:e>0?n.control_input.nextElementSibling:n.control_input.previousElementSibling)&&(s.classList.contains("active")&&n.removeActiveItem(i),n.setActiveItemClass(s)):n.moveCaret(e))}moveCaret(e){}getLastActive(e){let t=this.control.querySelector(".last-active")
if(t)return t
var i=this.control.querySelectorAll(".active")
return i?F(i,e):void 0}setCaret(e){this.caretPos=this.items.length}controlChildren(){return Array.from(this.control.querySelectorAll("[data-ts-item]"))}lock(){this.close(),this.isLocked=!0,this.refreshState()}unlock(){this.isLocked=!1,this.refreshState()}disable(){var e=this
e.input.disabled=!0,e.control_input.disabled=!0,e.focus_node.tabIndex=-1,e.isDisabled=!0,e.lock()}enable(){var e=this
e.input.disabled=!1,e.control_input.disabled=!1,e.focus_node.tabIndex=e.tabIndex,e.isDisabled=!1,e.unlock()}destroy(){var e=this,t=e.revertSettings
e.trigger("destroy"),e.off(),e.wrapper.remove(),e.dropdown.remove(),e.input.innerHTML=t.innerHTML,e.input.tabIndex=t.tabIndex,S(e.input,"tomselected","ts-hidden-accessible"),e._destroy(),delete e.input.tomselect}render(e,t){return"function"!=typeof this.settings.render[e]?null:this._render(e,t)}_render(e,t){var i,s,n=""
const o=this
return"option"!==e&&"item"!=e||(n=D(t[o.settings.valueField])),null==(s=o.settings.render[e].call(this,t,N))||(s=w(s),"option"===e||"option_create"===e?t[o.settings.disabledField]?P(s,{"aria-disabled":"true"}):P(s,{"data-selectable":""}):"optgroup"===e&&(i=t.group[o.settings.optgroupValueField],P(s,{"data-group":i}),t.group[o.settings.disabledField]&&P(s,{"data-disabled":""})),"option"!==e&&"item"!==e||(P(s,{"data-value":n}),"item"===e?(C(s,o.settings.itemClass),P(s,{"data-ts-item":""})):(C(s,o.settings.optionClass),P(s,{role:"option",id:t.$id}),o.options[n].$div=s))),s}clearCache(){y(this.options,((e,t)=>{e.$div&&(e.$div.remove(),delete e.$div)}))}uncacheValue(e){const t=this.getOption(e)
t&&t.remove()}canCreate(e){return this.settings.create&&e.length>0&&this.settings.createFilter.call(this,e)}hook(e,t,i){var s=this,n=s[t]
s[t]=function(){var t,o
return"after"===e&&(t=n.apply(s,arguments)),o=i.apply(s,arguments),"instead"===e?o:("before"===e&&(t=n.apply(s,arguments)),t)}}}return J.define("change_listener",(function(){B(this.input,"change",(()=>{this.sync()}))})),J.define("checkbox_options",(function(){var e=this,t=e.onOptionSelect
e.settings.hideSelected=!1
var i=function(e){setTimeout((()=>{var t=e.querySelector("input")
e.classList.contains("selected")?t.checked=!0:t.checked=!1}),1)}
e.hook("after","setupTemplates",(()=>{var t=e.settings.render.option
e.settings.render.option=(i,s)=>{var n=w(t.call(e,i,s)),o=document.createElement("input")
o.addEventListener("click",(function(e){H(e)})),o.type="checkbox"
const r=q(i[e.settings.valueField])
return r&&e.items.indexOf(r)>-1&&(o.checked=!0),n.prepend(o),n}})),e.on("item_remove",(t=>{var s=e.getOption(t)
s&&(s.classList.remove("selected"),i(s))})),e.hook("instead","onOptionSelect",((s,n)=>{if(n.classList.contains("selected"))return n.classList.remove("selected"),e.removeItem(n.dataset.value),e.refreshOptions(),void H(s,!0)
t.call(e,s,n),i(n)}))})),J.define("clear_button",(function(e){const t=this,i=Object.assign({className:"clear-button",title:"Clear All",html:e=>`<div class="${e.className}" title="${e.title}">&times;</div>`},e)
t.on("initialize",(()=>{var e=w(i.html(i))
e.addEventListener("click",(e=>{t.clear(),"single"===t.settings.mode&&t.settings.allowEmptyOption&&t.addItem(""),e.preventDefault(),e.stopPropagation()})),t.control.appendChild(e)}))})),J.define("drag_drop",(function(){var e=this
if(!$.fn.sortable)throw new Error('The "drag_drop" plugin requires jQuery UI "sortable".')
if("multi"===e.settings.mode){var t=e.lock,i=e.unlock
e.hook("instead","lock",(()=>{var i=$(e.control).data("sortable")
return i&&i.disable(),t.call(e)})),e.hook("instead","unlock",(()=>{var t=$(e.control).data("sortable")
return t&&t.enable(),i.call(e)})),e.on("initialize",(()=>{var t=$(e.control).sortable({items:"[data-value]",forcePlaceholderSize:!0,disabled:e.isLocked,start:(e,i)=>{i.placeholder.css("width",i.helper.css("width")),t.css({overflow:"visible"})},stop:()=>{t.css({overflow:"hidden"})
var i=[]
t.children("[data-value]").each((function(){this.dataset.value&&i.push(this.dataset.value)})),e.setValue(i)}})}))}})),J.define("dropdown_header",(function(e){const t=this,i=Object.assign({title:"Untitled",headerClass:"dropdown-header",titleRowClass:"dropdown-header-title",labelClass:"dropdown-header-label",closeClass:"dropdown-header-close",html:e=>'<div class="'+e.headerClass+'"><div class="'+e.titleRowClass+'"><span class="'+e.labelClass+'">'+e.title+'</span><a class="'+e.closeClass+'">&times;</a></div></div>'},e)
t.on("initialize",(()=>{var e=w(i.html(i)),s=e.querySelector("."+i.closeClass)
s&&s.addEventListener("click",(e=>{H(e,!0),t.close()})),t.dropdown.insertBefore(e,t.dropdown.firstChild)}))})),J.define("caret_position",(function(){var e=this
e.hook("instead","setCaret",(t=>{"single"!==e.settings.mode&&e.control.contains(e.control_input)?(t=Math.max(0,Math.min(e.items.length,t)))==e.caretPos||e.isPending||e.controlChildren().forEach(((i,s)=>{s<t?e.control_input.insertAdjacentElement("beforebegin",i):e.control.appendChild(i)})):t=e.items.length,e.caretPos=t})),e.hook("instead","moveCaret",(t=>{if(!e.isFocused)return
const i=e.getLastActive(t)
if(i){const s=L(i)
e.setCaret(t>0?s+1:s),e.setActiveItem()}else e.setCaret(e.caretPos+t)}))})),J.define("dropdown_input",(function(){var e=this
e.settings.shouldOpen=!0,e.hook("before","setup",(()=>{e.focus_node=e.control,C(e.control_input,"dropdown-input")
const t=w('<div class="dropdown-input-wrap">')
t.append(e.control_input),e.dropdown.insertBefore(t,e.dropdown.firstChild)})),e.on("initialize",(()=>{e.control_input.addEventListener("keydown",(t=>{switch(t.keyCode){case 27:return e.isOpen&&(H(t,!0),e.close()),void e.clearActiveItems()
case 9:e.focus_node.tabIndex=-1}return e.onKeyDown.call(e,t)})),e.on("blur",(()=>{e.focus_node.tabIndex=e.isDisabled?-1:e.tabIndex})),e.on("dropdown_open",(()=>{e.control_input.focus()}))
const t=e.onBlur
e.hook("instead","onBlur",(i=>{if(!i||i.relatedTarget!=e.control_input)return t.call(e)})),B(e.control_input,"blur",(()=>e.onBlur())),e.hook("before","close",(()=>{e.isOpen&&e.focus_node.focus()}))}))})),J.define("input_autogrow",(function(){var e=this
e.on("initialize",(()=>{var t=document.createElement("span"),i=e.control_input
t.style.cssText="position:absolute; top:-99999px; left:-99999px; width:auto; padding:0; white-space:pre; ",e.wrapper.appendChild(t)
for(const e of["letterSpacing","fontSize","fontFamily","fontWeight","textTransform"])t.style[e]=i.style[e]
var s=()=>{e.items.length>0?(t.textContent=i.value,i.style.width=t.clientWidth+"px"):i.style.width=""}
s(),e.on("update item_add item_remove",s),B(i,"input",s),B(i,"keyup",s),B(i,"blur",s),B(i,"update",s)}))})),J.define("no_backspace_delete",(function(){var e=this,t=e.deleteSelection
this.hook("instead","deleteSelection",(i=>!!e.activeItems.length&&t.call(e,i)))})),J.define("no_active_items",(function(){this.hook("instead","setActiveItem",(()=>{})),this.hook("instead","selectAll",(()=>{}))})),J.define("optgroup_columns",(function(){var e=this,t=e.onKeyDown
e.hook("instead","onKeyDown",(i=>{var s,n,o,r
if(!e.isOpen||37!==i.keyCode&&39!==i.keyCode)return t.call(e,i)
r=k(e.activeOption,"[data-group]"),s=L(e.activeOption,"[data-selectable]"),r&&(r=37===i.keyCode?r.previousSibling:r.nextSibling)&&(n=(o=r.querySelectorAll("[data-selectable]"))[Math.min(o.length-1,s)])&&e.setActiveOption(n)}))})),J.define("remove_button",(function(e){const t=Object.assign({label:"&times;",title:"Remove",className:"remove",append:!0},e)
var i=this
if(t.append){var s='<a href="javascript:void(0)" class="'+t.className+'" tabindex="-1" title="'+N(t.title)+'">'+t.label+"</a>"
i.hook("after","setupTemplates",(()=>{var e=i.settings.render.item
i.settings.render.item=(t,n)=>{var o=w(e.call(i,t,n)),r=w(s)
return o.appendChild(r),B(r,"mousedown",(e=>{H(e,!0)})),B(r,"click",(e=>{if(H(e,!0),!i.isLocked){var t=o.dataset.value
i.removeItem(t),i.refreshOptions(!1)}})),o}}))}})),J.define("restore_on_backspace",(function(e){const t=this,i=Object.assign({text:e=>e[t.settings.labelField]},e)
t.on("item_remove",(function(e){if(""===t.control_input.value.trim()){var s=t.options[e]
s&&t.setTextboxValue(i.text.call(t,s))}}))})),J.define("virtual_scroll",(function(){const e=this,t=e.canLoad,i=e.clearActiveOption,s=e.loadCallback
var n,o={},r=!1
if(!e.settings.firstUrl)throw"virtual_scroll plugin requires a firstUrl() method"
function l(t){return!("number"==typeof e.settings.maxOptions&&n.children.length>=e.settings.maxOptions)&&!(!(t in o)||!o[t])}e.settings.sortField=[{field:"$order"},{field:"$score"}],e.setNextUrl=function(e,t){o[e]=t},e.getUrl=function(t){if(t in o){const e=o[t]
return o[t]=!1,e}return o={},e.settings.firstUrl(t)},e.hook("instead","clearActiveOption",(()=>{if(!r)return i.call(e)})),e.hook("instead","canLoad",(i=>i in o?l(i):t.call(e,i))),e.hook("instead","loadCallback",((t,i)=>{r||e.clearOptions(),s.call(e,t,i),r=!1})),e.hook("after","refreshOptions",(()=>{const t=e.lastValue
var i
l(t)?(i=e.render("loading_more",{query:t}))&&i.setAttribute("data-selectable",""):t in o&&!n.querySelector(".no-results")&&(i=e.render("no_more_results",{query:t})),i&&(C(i,e.settings.optionClass),n.append(i))})),e.on("initialize",(()=>{n=e.dropdown_content,e.settings.render=Object.assign({},{loading_more:function(){return'<div class="loading-more-results">Loading more results ... </div>'},no_more_results:function(){return'<div class="no-more-results">No more results</div>'}},e.settings.render),n.addEventListener("scroll",(function(){n.clientHeight/(n.scrollHeight-n.scrollTop)<.95||l(e.lastValue)&&(r||(r=!0,e.load.call(e,e.lastValue)))}))}))})),J}))
var tomSelect=function(e,t){return new TomSelect(e,t)}
//# sourceMappingURL=tom-select.complete.min.js.map

View File

@@ -0,0 +1,334 @@
/**
* tom-select.css (v2.0.0-rc.4)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
.ts-wrapper.plugin-drag_drop.multi > .ts-control > div.ui-sortable-placeholder {
visibility: visible !important;
background: #f2f2f2 !important;
background: rgba(0, 0, 0, 0.06) !important;
border: 0 none !important;
box-shadow: inset 0 0 12px 4px #fff; }
.ts-wrapper.plugin-drag_drop .ui-sortable-placeholder::after {
content: '!';
visibility: hidden; }
.ts-wrapper.plugin-drag_drop .ui-sortable-helper {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); }
.plugin-checkbox_options .option input {
margin-right: 0.5rem; }
.plugin-clear_button .ts-control {
padding-right: calc( 1em + (3 * 6px)) !important; }
.plugin-clear_button .clear-button {
opacity: 0;
position: absolute;
top: 8px;
right: calc(8px - 6px);
margin-right: 0 !important;
background: transparent !important;
transition: opacity 0.5s;
cursor: pointer; }
.plugin-clear_button.single .clear-button {
right: calc(8px - 6px + 2rem); }
.plugin-clear_button.focus.has-items .clear-button,
.plugin-clear_button:hover.has-items .clear-button {
opacity: 1; }
.ts-wrapper .dropdown-header {
position: relative;
padding: 10px 8px;
border-bottom: 1px solid #d0d0d0;
background: #f8f8f8;
border-radius: 3px 3px 0 0; }
.ts-wrapper .dropdown-header-close {
position: absolute;
right: 8px;
top: 50%;
color: #303030;
opacity: 0.4;
margin-top: -12px;
line-height: 20px;
font-size: 20px !important; }
.ts-wrapper .dropdown-header-close:hover {
color: black; }
.plugin-dropdown_input.focus.dropdown-active .ts-control {
box-shadow: none;
border: 1px solid #d0d0d0; }
.plugin-dropdown_input .dropdown-input {
border: 1px solid #d0d0d0;
border-width: 0 0 1px 0;
display: block;
padding: 8px 8px;
box-shadow: none;
width: 100%;
background: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items .ts-control > input {
min-width: 0; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input {
flex: none;
min-width: 4px; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::-webkit-input-placeholder {
color: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::-ms-input-placeholder {
color: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::placeholder {
color: transparent; }
.ts-dropdown.plugin-optgroup_columns .ts-dropdown-content {
display: flex; }
.ts-dropdown.plugin-optgroup_columns .optgroup {
border-right: 1px solid #f2f2f2;
border-top: 0 none;
flex-grow: 1;
flex-basis: 0;
min-width: 0; }
.ts-dropdown.plugin-optgroup_columns .optgroup:last-child {
border-right: 0 none; }
.ts-dropdown.plugin-optgroup_columns .optgroup:before {
display: none; }
.ts-dropdown.plugin-optgroup_columns .optgroup-header {
border-top: 0 none; }
.ts-wrapper.plugin-remove_button .item {
display: inline-flex;
align-items: center;
padding-right: 0 !important; }
.ts-wrapper.plugin-remove_button .item .remove {
color: inherit;
text-decoration: none;
vertical-align: middle;
display: inline-block;
padding: 2px 6px;
border-left: 1px solid #d0d0d0;
border-radius: 0 2px 2px 0;
box-sizing: border-box;
margin-left: 6px; }
.ts-wrapper.plugin-remove_button .item .remove:hover {
background: rgba(0, 0, 0, 0.05); }
.ts-wrapper.plugin-remove_button .item.active .remove {
border-left-color: #cacaca; }
.ts-wrapper.plugin-remove_button.disabled .item .remove:hover {
background: none; }
.ts-wrapper.plugin-remove_button.disabled .item .remove {
border-left-color: white; }
.ts-wrapper.plugin-remove_button .remove-single {
position: absolute;
right: 0;
top: 0;
font-size: 23px; }
.ts-wrapper {
position: relative; }
.ts-dropdown,
.ts-control,
.ts-control input {
color: #303030;
font-family: inherit;
font-size: 13px;
line-height: 18px;
font-smoothing: inherit; }
.ts-control,
.ts-wrapper.single.input-active .ts-control {
background: #fff;
cursor: text; }
.ts-control {
border: 1px solid #d0d0d0;
padding: 8px 8px;
width: 100%;
overflow: hidden;
position: relative;
z-index: 1;
box-sizing: border-box;
box-shadow: none;
border-radius: 3px;
display: flex;
flex-wrap: wrap; }
.ts-wrapper.multi.has-items .ts-control {
padding: calc( 8px - 2px - 0) 8px calc( 8px - 2px - 3px - 0); }
.full .ts-control {
background-color: #fff; }
.disabled .ts-control,
.disabled .ts-control * {
cursor: default !important; }
.focus .ts-control {
box-shadow: none; }
.ts-control > * {
vertical-align: baseline;
display: inline-block; }
.ts-wrapper.multi .ts-control > div {
cursor: pointer;
margin: 0 3px 3px 0;
padding: 2px 6px;
background: #f2f2f2;
color: #303030;
border: 0 solid #d0d0d0; }
.ts-wrapper.multi .ts-control > div.active {
background: #e8e8e8;
color: #303030;
border: 0 solid #cacaca; }
.ts-wrapper.multi.disabled .ts-control > div, .ts-wrapper.multi.disabled .ts-control > div.active {
color: #7d7c7c;
background: white;
border: 0 solid white; }
.ts-control > input {
flex: 1 1 auto;
min-width: 7rem;
display: inline-block !important;
padding: 0 !important;
min-height: 0 !important;
max-height: none !important;
max-width: 100% !important;
margin: 0 !important;
text-indent: 0 !important;
border: 0 none !important;
background: none !important;
line-height: inherit !important;
-webkit-user-select: auto !important;
-moz-user-select: auto !important;
-ms-user-select: auto !important;
user-select: auto !important;
box-shadow: none !important; }
.ts-control > input::-ms-clear {
display: none; }
.ts-control > input:focus {
outline: none !important; }
.has-items .ts-control > input {
margin: 0 4px !important; }
.ts-control.rtl {
text-align: right; }
.ts-control.rtl.single .ts-control:after {
left: 15px;
right: auto; }
.ts-control.rtl .ts-control > input {
margin: 0 4px 0 -2px !important; }
.disabled .ts-control {
opacity: 0.5;
background-color: #fafafa; }
.input-hidden .ts-control > input {
opacity: 0;
position: absolute;
left: -10000px; }
.ts-dropdown {
position: absolute;
top: 100%;
left: 0;
width: 100%;
z-index: 10;
border: 1px solid #d0d0d0;
background: #fff;
margin: 0.25rem 0 0 0;
border-top: 0 none;
box-sizing: border-box;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-radius: 0 0 3px 3px; }
.ts-dropdown [data-selectable] {
cursor: pointer;
overflow: hidden; }
.ts-dropdown [data-selectable] .highlight {
background: rgba(125, 168, 208, 0.2);
border-radius: 1px; }
.ts-dropdown .option,
.ts-dropdown .optgroup-header,
.ts-dropdown .no-results,
.ts-dropdown .create {
padding: 5px 8px; }
.ts-dropdown .option, .ts-dropdown [data-disabled], .ts-dropdown [data-disabled] [data-selectable].option {
cursor: inherit;
opacity: 0.5; }
.ts-dropdown [data-selectable].option {
opacity: 1;
cursor: pointer; }
.ts-dropdown .optgroup:first-child .optgroup-header {
border-top: 0 none; }
.ts-dropdown .optgroup-header {
color: #303030;
background: #fff;
cursor: default; }
.ts-dropdown .create:hover,
.ts-dropdown .option:hover,
.ts-dropdown .active {
background-color: #f5fafd;
color: #495c68; }
.ts-dropdown .create:hover.create,
.ts-dropdown .option:hover.create,
.ts-dropdown .active.create {
color: #495c68; }
.ts-dropdown .create {
color: rgba(48, 48, 48, 0.5); }
.ts-dropdown .spinner {
display: inline-block;
width: 30px;
height: 30px;
margin: 5px 8px; }
.ts-dropdown .spinner:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 3px;
border-radius: 50%;
border: 5px solid #d0d0d0;
border-color: #d0d0d0 transparent #d0d0d0 transparent;
animation: lds-dual-ring 1.2s linear infinite; }
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg); }
100% {
transform: rotate(360deg); } }
.ts-dropdown-content {
overflow-y: auto;
overflow-x: hidden;
max-height: 200px;
overflow-scrolling: touch;
scroll-behavior: smooth; }
.ts-hidden-accessible {
border: 0 !important;
clip: rect(0 0 0 0) !important;
-webkit-clip-path: inset(50%) !important;
clip-path: inset(50%) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important; }
/*# sourceMappingURL=tom-select.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,22 +1,21 @@
from pyvis.network import Network from pyvis.network import Network
import pyvis
from random import randint
pyvis.__version__
def random_colour():
return f"rgb({randint(0,255)},{randint(0,255)},{randint(0,255)})"
def main(): def main():
net = Network(notebook=False, cdn_resources="remote", neighborhood_highlight=True, bgcolor="#222222", font_color="#FAFAFA") net = Network(notebook=False, cdn_resources="local", neighborhood_highlight=True, bgcolor="#222222", font_color="#FAFAFA", layout=True)
net.repulsion(spring_length=300)
net.add_nodes([1,2,3,4,5,6,7,8,9,10], label=["1","2","3","4","5","6","7","8","9","10"],color=[random_colour() for _ in range(10)]) net.add_nodes([0,1,2,3,4,5,6], label=['1','2','3','4','5','6','7'],color=['#E0E0FF','#E0FFE0','#E0E0FF','#E0E0FF','#E0E0FF','#E0E0FF','#E0FFE0'])
net.add_edges([(1, 2, 10), (1, 3, 5), (1, 4, 6), (2, 3, 10), (2, 5, 1), (5, 6, 9), (7, 8, 7), (8, 9, 7), (9, 7, 8), (1, 7, 7), (9, 10, 2)]) net.add_edge(0, 1, length=500)
net.add_edge(1, 2, length=750)
net.add_edge(2, 3, length=500)
net.add_edge(3, 4, length=250)
net.add_edge(2, 4, length=250)
net.add_edge(4, 5, length=750)
net.add_edge(5, 6, length=500)
net.add_edge(1, 6, length=0)
net.show("network.html", notebook=False)
net.write_html("network.html", notebook=False)
if __name__ == "__main__": if __name__ == "__main__":