CNX Software recenserar: Kom igång med GL-S200-paketet med gränsrouter för Thread
Förra veckan tittade vi på maskinvaran i GL.iNet GL-S200-paketet med gränsrouter för Thread och tre nRF52840 Thread Dev Boards, och nu har jag haft tid att arbeta med paketet och berättar om mina första erfarenheter i recensionens andra del.
Första konfigurationen av GL-S200
Jag anslöt WAN-porten till min Ethernet-switch, som i sin tur är ansluten till modemroutern, och LAN-porten till min bärbara dator för att nå webbgränssnittet på standardadressen (192.168.8.1). GL-S200 använder samma Admin Panel som andra GL.iNet-routrar, till exempel Beryl AX-routern som vi recenserade i början av året.
En guide hjälper dig att välja språk och ange ett nytt lösenord för Admin Panel. Därefter får du åtkomst till den välbekanta GL.iNet Admin Panel 4.x.x.
När guiden är klar bör systemet ansluta automatiskt till internet och den mittersta RGB-lysdioden på enheten bli grön.
Thread-nätverk
Första steget är att gå till THREAD MESH i vänsterpanelen och aktivera Thread-nätverket.
När du klickar på Enable får du tre IPv6-adresser:
-
Link-Local Address – alla gränssnitt som kan nås med en enda radioöverföring
-
Local Address –
alla gränssnitt som kan nås utifrån ett Thread-nätverk (det verkar kallas Global Address i OpenThread-dokumentationen)Uppdatering från GL-iNet: OT/OTBR stöder ännu inte åtkomst till det globala IPv6-internet.
Du kan läsa Openthreads officiella svar: https://github.com/openthread/openthread/discussions/8794
Den lokala adressen är en OMR(off-mesh-routable)-adress som låter dig nå Thread-enheter från wifi/ethernet-sidan.
-
Mesh-Local EID – alla gränssnitt som kan nås inom samma Thread-nätverk
Om du har egna Thread-enheter kan du gå till menyn Thread Commissioning och ange enheternas EUI64 och den fördelade nyckeln. GL.iNet har dock gjort en enklare del av webbgränssnittet för sina Thread Dev Boards, eller kort sagt TBD.
I avsnittet GL DEV BOARD->Devices kan vi klicka på knappen Add Devices och sedan Apply. Om vi trycker på SW2 på ett kort läggs det automatiskt till i gränsroutern GL-S200 för Thread.
Vi väljer typen A0+A1 och namnger kortet. Sedan upprepar vi det för de två andra korten och ser temperatur, tryck, luftfuktighet och ljusstyrka från alla tre i Admin Panel.
Firmwareuppdatering av GL-S200
Då kom jag ihåg att jag glömt något… GL.iNet hade meddelat att det fanns ny firmware för GL-S200 som jag skulle installera manuellt. Vi gör det nu genom att gå till ”SYSTEM->Upgrade”.
Admin Panel visar att firmware 4.1.3 release 5 är aktuell eftersom den nya firmwaren inte har skickats till OTA-servern. Men jag fick en nedladdningslänk till nyare firmware som kompilerades den 6 mars 2023.
Jag laddade ned filen, gick till Local Upgrade och laddade upp den till gränsroutern GL-S200 för Thread.
Verifieringen lyckades, så jag klickade på Installoch till slut fick jag Firmware 4.1.3 Release7 installerad på enheten.
Uppgradera firmware i Thread Dev Boards
Under firmwareuppdateringen såg jag också att Thread Dev Boards kunde uppdateras från version 1.1.5 till 1.1.9. Jag gjorde det och allt fungerade felfritt.
Thread-topologi
Thread bygger på meshnätverk. Noder nära routern ansluter direkt, medan noder utom räckhåll eller med svag signal själva kan fungera som routrar. Med alla enheter på skrivbordet…
… hade nätverket som väntat en stjärntopologi.
Jag flyttade därför isär korten och placerade dem i en rak linje, med ett av dem utomhus.
Det är lätt att se vilket som är utomhus på den högre temperaturen och ljusstyrkan.
Klickar vi på ”Self” i topologidiagrammet ser vi också signalstyrkan för noden som är ansluten till gränsroutern för Thread. Jag lyckades dock aldrig använda Thread Dev Board som router. Vi gick till och med ut för att komma utom räckhåll eller åtminstone få svagare signal, men meshnätverket fungerade inte som förväntat. Jag frågade därför GL.iNets tekniker, som svarade att det krävdes thread router-firmware:
För närvarande är vår standardfirmware för TBD av typen thread end device och enheterna kan inte anslutas direkt till varandra. Vi skickar er thread router firmware och motsvarande guide för att skriva TBD-firmware senare i dag.
De skickade routerfirmwaren och verktyget mcumgr för Linux och Windows.
Jag anslöt ett av korten Thread Dev Boards till en Linux-PC (Ubuntu 22.04):
print(“Hello World”) visas som print(“Hello World”)`.
$ bt -l
port | age (sec) | device | driver | description
------+------------+------------+------------------+----------------------
* 0 | 1137 | ttyUSB0 | ch341-uart | USB Serial
Serieporten identifieras, så jag kan köra kommandot för att kontrollera aktuell firmwarestatus:
$ sudo ./mcumgr --conntype serial --connstring=/dev/ttyUSB0,baud=460800 image list
Error: NMP timeout
jaufranc@cnx-laptop-4:~/edev/GL-S200/TBD upgrade$ sudo ./mcumgr --conntype serial --connstring=/dev/ttyUSB0,baud=460800 image list
Images:
image=0 slot=0
version: 1.1.9
bootable: true
flags: active confirmed
hash: 88b4725af7975e0b60915c320a82082bb2dda923b736f0e31d2c7d4d67cac3d3
image=0 slot=1
version: 1.1.5
bootable: true
flags:
hash: 4d8548e6910e7214d8325a7d1a49a8390fb25cce983bdb0b15bfecc657f7860b
Split status: N/A (0)
Första försöket misslyckades med felet ”NMP timeout”, men det andra fungerade. Firmware 1.1.9 som jag uppdaterade i Admin Panel finns i plats 0 och tidigare firmware 1.1.5 i plats 1. Nu kan jag försöka skriva Router-firmwaren:
$ sudo ./mcumgr --conntype serial --connstring=/dev/ttyUSB0,baud=460800 image upload GL-Thread-Dev-Board-FTD-OTA-v1.1.9.bin
426.58 KiB / 426.58 KiB [========================] 100.00% 8.93 KiB/s 47s
Done
Vi kontrollerar igen:
$ sudo ./mcumgr --conntype serial --connstring=/dev/ttyUSB0,baud=460800 image list
Images:
image=0 slot=0
version: 1.1.9
bootable: true
flags: active confirmed
hash: 88b4725af7975e0b60915c320a82082bb2dda923b736f0e31d2c7d4d67cac3d3
image=0 slot=1
version: 1.1.9
bootable: true
flags:
hash: 7dbb8a9867a000bb968099593bc1f6c2c7bb28ab458719f499364c4a77feca9f
Split status: N/A (0)
Done
Firmware i plats 0 körs nu, medan firmware i plats 1 är Router-versionen vi just skrev. Vi kan växla till den nya firmwaren så här:
sudo ./mcumgr --conntype serial --connstring=/dev/ttyUSB0,baud=460800 image test 7dbb8a9867a000bb968
Sedan kan vi starta om kortet och kontrollera att den nya firmwaren med hash som börjar på 7dbb finns i plats 0:
$ sudo ./mcumgr --conntype serial --connstring=/dev/ttyUSB0,baud=460800 image list
Images:
image=0 slot=0
version: 1.1.9
bootable: true
flags: active confirmed
hash: 7dbb8a9867a000bb968099593bc1f6c2c7bb28ab458719f499364c4a77feca9f
image=0 slot=1
version: 1.1.9
bootable: true
flags:
hash: 88b4725af7975e0b60915c320a82082bb2dda923b736f0e31d2c7d4d67cac3d3
Split status: N/A (0)
När kortet lagts till igen i Admin Panel ser vi Thread Leader (GL-S200), två End Devices och en Router (kortet där vi just skrev Router-firmwaren).
Ingen av End Devices är ansluten till routern Board 1 eftersom alla ligger på skrivbordet och är anslutna till GL-S200. Jag flyttar därför Board 1 och en av End Devices till ett annat rum, 12 meter från gränsroutern GL-S200 för Thread. Topologin ändras dock inte. Anslutningen verkar ”klibbig”: en Thread-nod återansluter bara till en annan Router när den är utom räckhåll, inte enbart utifrån signalstyrkan.
När jag däremot stängde av GL-S200 och startade den igen hade de två End Devices återanslutit till routern Board 1, som nu var nätverkets ”Thread Leader”.
Det fungerar alltså. Jag väntade mig inte just detta beteende eftersom Board 2 är 10 centimeter från gränsroutern GL-S200 för Thread (self) och Board 1 är 12 meter bort, men Board 2 återanslöt inte till GL-S200 ens efter en stund. Så länge anslutningen är aktiv återansluter en Thread-nod alltså inte till en annan router även om den är närmare och har starkare signal. Jag antar att det är rimligt för batteritiden.
Jag tog därför Board 2 (End Device) och Board 1 (Router) på en kort tur utom GL-S200-enhetens räckvidd, slog först på Board 1 och sedan Board 2 för att säkerställa sammankoppling och gick hem igen. Det fungerade: i diagrammet ovan ansluter Board 2 till GL-S200 ”Thread Leader” via Board 1 som router.
Information om GL Thread Dev Board
Klickar vi på de tre punkterna i kolumnen Action för en enhet får vi alternativen Device Detail, View Records, Edit Device, Get Code och Reset Device.
Enhetsinformation
Vi får EUI-64, namn, IPv6-adress, Extended MAC-adress (64-bitars i stället för vanliga 48-bitars), firmwareversion och sensordata.
View Records
Efter att korten körts i över 24 timmar öppnade jag menyn ”View Records” för att se diagram över sensorvärdena för ett visst kort.
Det ser mestadels bra ut, men längst ned till vänster finns värden klockan 9 på kvällen medan diagrammet av någon anledning börjar först klockan 11 på kvällen. Det är samma sak för alla kort.
Timdiagrammet ser ännu sämre ut. Det verkar vara ett visningsfel snarare än saknade datapunkter eftersom vi alltid kan få fram en datapunkt till vänster.
Kodexempel
”Get Code” kan vara det viktigaste avsnittet om du vill övervaka och styra egna Thread-enheter. Det finns fem kodexempel.
Vi behöver ansluta till gränsroutern GL-S200 för Thread via SSH för att köra exemplen. Vi börjar med koden för att läsa sensordata.
root@GL-S200:~# /usr/bin/demo_get_sensor_data
1 dev_id=9483c44004df0679 connected=1 temperature=31.300811 humidity=43.409729 brightness=0 pressure=97.817392
2 dev_id=9483c47d79c19475 connected=1 temperature=32.742767 humidity=53.620910 brightness=0 pressure=97.814472
3 dev_id=9483c4ec736c4f68 connected=1 temperature=31.292800 humidity=42.652893 brightness=0 press
Vi kan också välja en viss enhet med dess EUI-64:
root@GL-S200:~# /usr/bin/demo_get_sensor_data 9483c44004df0679
dev_id=9483c44004df0679 connected=1 temperature=31.300811 humidity=43.409729 brightness=0 pre
Här är källkoden som referens:
#!/bin/sh
# Note, this file is stored in /usr/bin/demo_get_sensor_data
# Usage: demo_get_sensor_data <dev_id>
# It will print all sensor data if <dev_id> is null, and the <dev_id> is the EUI64 of the device, which saved in /etc/config/thread_devices
. /usr/share/libubox/jshn.sh
dev_id=$1
if [ -z "$dev_id" ]; then
res=$(curl -s localhost/rpc -d '{"jsonrpc":"2.0","id":"0","method":"call","params":["","gw","get_device_list",{}]}')
json_init
json_load "$res"
json_select result
json_select device_list
idx=1
while json_is_a ${idx} object; do
json_select ${idx}
json_get_var dev_id dev_id
json_get_var connected connected
json_select dev_data
json_get_var val_temp temperature
json_get_var val_humi humidity
json_get_var val_brightness brightness
json_get_var val_pressure pressure
echo "$idx dev_id=$dev_id connected=$connected temperature=$val_temp humidity=$val_humi brightness=$val_brightness pressure=$val_pressure"
json_select ..
json_select ..
idx=$((idx + 1))
done
else
res=$(curl -s localhost/rpc -d "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"call\",\"params\":[\"\",\"gw\",\"get_device_status\",{\"dev_id\":\"$dev_id\"}]}")
json_init
json_load "$res"
json_select result
json_get_var dev_id dev_id
json_get_var connected connected
json_select dev_data
json_get_var val_temp temperature
json_get_var val_humi humidity
json_get_var val_brightness brightness
json_get_var val_pressure pressure
echo "dev_id=$dev_id connected=$connected temperature=$val_temp humidity=$val_humi brightness=$val_brightness pressure=$val_pressure"
fi
Vi testar de andra exemplen.
ls /usr/bin/demo*
/usr/bin/demo_control_gpio /usr/bin/demo_sensor_trigger
/usr/bin/demo_control_led /usr/bin/demo_user_action_trigger
/usr/bin/demo_get_sensor_data
Det andra exemplet styr de två RGB-lysdioderna på kortet som valts med sitt EUI-64-enhets-ID:
root@GL-S200:~# /usr/bin/demo_control_led 9483c44004df0679
LED OFF
LED ON
LED OFF
LED TOGGLE
Changle LED Color to Red light
Changle LED Color to Orange light
Changle LED Color to Yellow light
Changle LED Color to Green light
Changle LED Color to Blue light
Changle LED Color to Indigo light
Changle LED Color to Purple light
Changle LED Color to White light
Här är en kort videodemonstration av hur det ser ut.
GPIO-styrningsexemplet sätter vissa I/O i utgångsläge och sedan lågt eller högt, återigen för ett visst kort:
root@GL-S200:~# /usr/bin/demo_control_gpio 9483c44004df0679
Get GPIO Status
GPIO 0.15: 0
GPIO 0.16: 0
GPIO 0.17: 0
GPIO 0.20: 0
Set GPIO 0.15/0.16/0.17/0.20 to output low level
Get GPIO Status
GPIO 0.15: 0
GPIO 0.16: 0
GPIO 0.17: 0
GPIO 0.20: 0
Set GPIO 0.15/0.16/0.17/0.20 to output high level
Get GPIO Status
GPIO 0.15: 1
GPIO 0.16: 1
GPIO 0.17: 1
GPIO 0.20: 1
De två senaste exemplen för LED- och GPIO-styrning använder kommandot coap_cli, en ”liten CoAP-implementation”, till exempel för att släcka lysdioderna:
coap_cli -B 2 -N -e "$ctl_cmd_led_off" -m put coap://[$addr]/cmd 1>/dev/null 2>/
Användning av coap_cli:
root@GL-S200:~# coap_cli
coap_cli v4.2.1 -- a small CoAP implementation
Copyright (C) 2010-2019 Olaf Bergmann <bergmann@tzi.org> and others
TLS Library: None
Usage: coap_cli [-a addr] [-b [num,]size] [-e text] [-f file] [-l loss]
[-m method] [-o file] [-p port] [-r] [-s duration] [-t type]
[-v num] [-A type] [-B seconds] [-K interval] [-N] [-O num,text]
[-P addr[:port]] [-T token] [-U]
[[-k key] [-u user]]
[[-c certfile] [-C cafile] [-R root_cafile]] URI
URI can be an absolute URI or a URI prefixed with scheme and host
General Options
-a addr The local interface address to use
-b [num,]size Block size to be used in GET/PUT/POST requests
(value must be a multiple of 16 not larger than 1024)
If num is present, the request chain will start at
block num
-e text Include text as payload (use percent-encoding for
non-ASCII characters)
-f file File to send with PUT/POST (use '-' for STDIN)
-l list Fail to send some datagrams specified by a comma
separated list of numbers or number ranges
(for debugging only)
-l loss% Randomly fail to send datagrams with the specified
probability - 100% all datagrams, 0% no datagrams
-m method Request method (get|put|post|delete|fetch|patch|ipatch),
default is 'get'
-o file Output received data to this file (use '-' for STDOUT)
-p port Listen on specified port
-r Use reliable protocol (TCP or TLS)
-s duration Subscribe to / Observe resource for given duration
in seconds
-t type Content format for given resource for PUT/POST
-v num Verbosity level (default 3, maximum is 9). Above 7,
there is increased verbosity in GnuTLS logging
-A type Accepted media type
-B seconds Break operation after waiting given seconds
(default is 90)
-K interval send a ping after interval seconds of inactivity
-N Send NON-confirmable message
-O num,text Add option num with contents text to request. If the
text begins with 0x, then the hex text is converted to
binary data
-P addr[:port] Use proxy (automatically adds Proxy-Uri option to
request)
-T token Include specified token
-U Never include Uri-Host or Uri-Port options
PSK Options (if supported by underlying (D)TLS library)
-k key Pre-shared key for the specified user
-u user User identity for pre-shared key mode
PKI Options (if supported by underlying (D)TLS library)
-c certfile PEM file containing both CERTIFICATE and PRIVATE KEY
This argument requires (D)TLS with PKI to be available
-C cafile PEM file containing the CA Certificate that was used to
sign the certfile. This will trigger the validation of
the server certificate. If certfile is self-signed (as
defined by '-c certfile'), then you need to have on the
command line the same filename for both the certfile and
cafile (as in '-c certfile -C certfile') to trigger
validation
-R root_cafile PEM file containing the set of trusted root CAs that
are to be used to validate the server certificate.
The '-C cafile' does not have to be in this list and is
'trusted' for the verification.
Alternatively, this can point to a directory containing
a set of CA PEM files
Examples:
coap-client -m get coap://[::1]/
coap-client -m get coap://[::1]/.well-known/core
coap-client -m get coap+tcp://[::1]/.well-known/core
coap-client -m get coaps://[::1]/.well-known/core
coap-client -m get coaps+tcp://[::1]/.well-known/core
coap-client -m get -T cafe coap://[::1]/time
echo -n 1000 | coap-client -m put -T cafe coap://[::1]/time -f -
Demonstrationen av åtgärdsutlösaren visar vredets status:
root@GL-S200:~# /usr/bin/demo_user_action_trigger 9483c44004df0679
2023/03/19 17:38:14 info (/usr/bin/demo_user_action_trigger:34) 9483c44004df0679: Press the knob.
2023/03/19 17:38:16 info (/usr/bin/demo_user_action_trigger:34) 9483c44004df0679: Press the knob.
2023/03/19 17:38:18 info (/usr/bin/demo_user_action_trigger:32) 9483c44004df0679: Turn the knob forward
2023/03/19 17:38:19 info (/usr/bin/demo_user_action_trigger:32) 9483c44004df0679: Turn the knob forward
2023/03/19 17:38:20 info (/usr/bin/demo_user_action_trigger:32) 9483c44004df0679: Turn the knob forward
2023/03/19 17:38:20 info (/usr/bin/demo_user_action_trigger:32) 9483c44004df0679: Turn the knob reverse
2023/03/19 17:38:21 info (/usr/bin/demo_user_action_trigger:32) 9483c44004df0679: Turn the knob reverse
2023/03/19 17:38:21 info (/usr/bin/demo_user_action_trigger:32) 9483c44004df0679: Turn the knob reverse
2023/03/19 17:38:23 info (/usr/bin/demo_user_action_trigger:34) 9483c44004df0679: Press the knob.
2023/03/19 17:38:25 info (/usr/bin/demo_user_action_trigger:32) 9483c44004df0679: Turn the
Koden bygger på binärfilen /usr/bin/eco [Uppdatering: det verkar vara tolkprojektet eco-lua Lua , se kommentarsfältet]:
#!/usr/bin/env eco
-- Note, this file is stored in /usr/bin/demo_user_action_trigger, It listens for events reported when the user operates the knob.
-- Usage: demo_sensor_trigger
local ubus = require 'eco.ubus'
local sys = require 'eco.sys'
local cjson = require 'cjson'
local log = require "glog"
local OPT_TURNING_THE_KNOB = 1
local OPT_PRESS_THE_KNOB = 2
log.level(log.LOG_INFO)
sys.signal(sys.SIGINT, function()
print('
Got SIGINT, now quit')
eco.unloop()
end)
local con, err = ubus.connect()
if not con then
error(err)
end
con:listen('user_active_trigger', function(ev, msg)
local event_data = cjson.encode(msg)
log.debug('Recievd user active trigger ' .. ev .. ' ' .. event_data)
local trigger, operation, direction = msg.trigger, msg.operation, msg.direction
if operation == OPT_TURNING_THE_KNOB then
log.info(trigger .. ': Turn the knob ' .. direction)
elseif operation == OPT_PRESS_THE_KNOB then
log.info(trigger .. ': Press the knob.')
else
log.err('Unknown operation!')
end
end)
Detsamma gäller exemplet med sensorutlösare som övervakar PIR-sensorn för alla Thread Dev Boards anslutna till gränsroutern GL-S200 för Thread:
root@GL-S200:~# /usr/bin/demo_sensor_trigger
2023/03/19 17:47:46 info (/usr/bin/demo_sensor_trigger:31) 9483c44004df0679: Human body detected.
2023/03/19 17:47:46 info (/usr/bin/demo_sensor_trigger:31) 9483c4ec736c4f68: Human body detected.
2023/03/19 17:47:49 info (/usr/bin/demo_sensor_trigger:31) 9483c4ec736c4f68: Human body detected.
2023/03/19 17:47:52 info (/usr/bin/demo_sensor_trigger:31) 9483c44004df0679: Human body detected.
2023/03/19 17:48:15 info (/usr/bin/demo_sensor_trigger:31) 9483c47d79c19475: Human body detected.
Alla dessa exempel kan hjälpa dig att komma igång med egna skript.
GL-S200 ”Automations”
GL DEV BOARDS innehåller också avsnittet ”Automations”.
Jag ska försöka styra RGB-lysdioderna på två Thread Dev Boards med vredet på det tredje kortet.
När automationen fått ett namn kan vi välja användaråtgärd och sedan kortet vars vred används: ”Board 2”.
Jag valde ”Turning the knob” och sedan ”Device(s) Action”.
Eftersom jag vill styra de två andra korten valde jag ”Board 1” och ”Board 2”.
Det enda alternativet är ”Change Color”, så vi väljer det och klickar på Apply.
Men det fungerar inte. RGB-lysdioderna förblir släckta oavsett vilket håll jag vrider vredet. Till slut upptäckte jag att lysdioderna måste vara tända innan färgen kan ändras. Jag skapade därför en annan automation som tänder och släcker dem när vredet trycks ned.
Nu fungerar det.
Automation-avsnittet stöder också webhooks, så jag skapade en automation för att meddela mig i Discord när rörelse upptäcks. Jag skapade en Discord-server och fick en Webhook URL(https://discord.com/api/webhooks/xxxx) genom att följa instruktionerna på Discord.
Sedan skapade jag en annan automation och valde Sensor Trigger…
… ”Human Body Detected” (vilket bara betyder att PIR-rörelsesensorn utlösts)
… och ”Webhook”.
Där lade jag till min Webhook URL, valde Json och angav innehåll som:
{"content":"OMG! Somebody has just entered the bedroom!"}
Till sist klickade jag på Apply och upprepade proceduren för att upptäcka rörelse i köket och på kontoret.
Nu kan jag öppna Discord och gå runt i huset för att utlösa PIR-rörelsesensorerna på alla tre kort.
Det lyckades! Automationsdelen är riktigt smidig men verkar bara fungera med GL.iNet Thread Dev Boards. Jag frågade därför om källkoden skulle delas så att andra kan anpassa lösningen till egna Thread-enheter, men fick inget svar på just den frågan.
Övrigt
Precis som andra GL.iNet-nätverksenheter stöder GL-S200 GoodCloud för fjärråtkomst, men nyttan är begränsad eftersom Thread- och Bluetooth-delarna inte stöds.
Jag trodde först att knappen Mode Switch kunde växla mellan BLE och Thread, men precis som på tidigare routrar kan den ställas in för att aktivera eller inaktivera WireGuard eller OpenVPN.
Av tidsskäl har jag inte testat Bluetooth-delen, men jag förväntar mig ett liknande gränssnitt och arbetssätt som i GL-S10 BLE-till-MQTT-bryggan som jag recenserade förra året.
Det var en intressant erfarenhet, och jag tackar GL.iNet för GL-S200-paketet med gränsrouter för Thread som skickades för utvärdering och betatestning. Paketet jag fick kan förhandsbeställas för $154.00 plus frakt, men om du redan har egna Thread-noder kan GL-S200 ensamt köpas för $79 under förbeställningsperioden, varefter priset blir $99.
|
Jean-Luc Aufranc (CNXSoft)Jean-Luc startade CNX Software 2010 som en deltidssyssla, innan han slutade som programvaruutvecklingschef och senare under 2011 började skriva nyheter och recensioner på heltid. |
About GL.iNet
Founded in 2010, GL.iNet is a leading provider of innovative routers and networking solutions. From compact travel routers to cutting-edge remote KVMs, GL.iNet delivers award-winning networking technology designed for performance and security.
GL.iNet gives people and organizations the power to connect with confidence. By putting control in the hands of users, GL.iNet supports the productivity and collaboration that shape a Good Life.