From 2cf0fd9d215dda17eae4261ab7967367f6aa0028 Mon Sep 17 00:00:00 2001 From: Eugene Crosser Date: Sun, 27 Nov 2022 00:05:37 +0100 Subject: [PATCH] Implement sending commands from the web interface --- loctrkd/beesure.py | 17 ++++++++++++--- loctrkd/common.py | 18 +++++++++++++++- loctrkd/evstore.py | 29 +++++++++++++++++++++++++- loctrkd/protomodule.py | 8 +++++++ loctrkd/rectifier.py | 6 ++++-- loctrkd/storage.py | 4 +++- loctrkd/wsgateway.py | 47 ++++++++++++++++++++++++++++++++++++++++-- loctrkd/zmsg.py | 17 ++++++++++----- loctrkd/zx303proto.py | 19 +++++++++++------ 9 files changed, 144 insertions(+), 21 deletions(-) diff --git a/loctrkd/beesure.py b/loctrkd/beesure.py index dcf16ba..e782c73 100755 --- a/loctrkd/beesure.py +++ b/loctrkd/beesure.py @@ -43,6 +43,7 @@ __all__ = ( "Respond", ) +MODNAME = __name__.split(".")[-1] PROTO_PREFIX = "BS:" ### Deframer ### @@ -380,12 +381,12 @@ class _LOC_DATA(BeeSurePkt): self.latitude = p.lat * p.nors self.longitude = p.lon * p.eorw - def rectified(self) -> Report: + def rectified(self) -> Tuple[str, Report]: # self.gps_valid is supposed to mean it, but it does not. Perfectly # good looking coordinates, with ten satellites, still get 'V'. # I suspect that in reality, 'A' means "hint data is absent". if self.gps_valid or self.num_of_sats > 3: - return CoordReport( + return MODNAME, CoordReport( devtime=str(self.devtime), battery_percentage=self.battery_percentage, accuracy=self.positioning_accuracy, @@ -396,7 +397,7 @@ class _LOC_DATA(BeeSurePkt): longitude=self.longitude, ) else: - return HintReport( + return MODNAME, HintReport( devtime=str(self.devtime), battery_percentage=self.battery_percentage, mcc=self.mcc, @@ -679,3 +680,13 @@ def exposed_protos() -> List[Tuple[str, bool]]: for cls in CLASSES.values() if hasattr(cls, "rectified") ] + + +def make_response(cmd: str, imei: str, **kwargs: Any) -> Optional[BeeSurePkt]: + if cmd == "poweroff": + return POWEROFF.Out() + elif cmd == "refresh": + return MONITOR.Out() + elif cmd == "message": + return MESSAGE.Out(message=kwargs.get("txt", "Hello")) + return None diff --git a/loctrkd/common.py b/loctrkd/common.py index 941a93e..162fe0d 100644 --- a/loctrkd/common.py +++ b/loctrkd/common.py @@ -11,7 +11,7 @@ from sys import argv, stderr, stdout from typing import Any, cast, Dict, List, Optional, Tuple, Union from types import SimpleNamespace -from .protomodule import ProtoModule +from .protomodule import ProtoClass, ProtoModule CONF = "/etc/loctrkd.conf" pmods: List[ProtoModule] = [] @@ -71,6 +71,22 @@ def pmod_for_proto(proto: str) -> Optional[ProtoModule]: return None +def pmod_by_name(pmodname: str) -> Optional[ProtoModule]: + for pmod in pmods: + if pmod.__name__.split(".")[-1] == pmodname: + return pmod + return None + + +def make_response( + pmodname: str, cmd: str, imei: str, **kwargs: Any +) -> Optional[ProtoClass.Out]: + pmod = pmod_by_name(pmodname) + if pmod is None: + return None + return pmod.make_response(cmd, imei, **kwargs) + + def parse_message(proto: str, packet: bytes, is_incoming: bool = True) -> Any: pmod = pmod_for_proto(proto) return pmod.parse_message(packet, is_incoming) if pmod else None diff --git a/loctrkd/evstore.py b/loctrkd/evstore.py index 85e8c9d..6d6edd0 100644 --- a/loctrkd/evstore.py +++ b/loctrkd/evstore.py @@ -3,7 +3,7 @@ from datetime import datetime from json import dumps, loads from sqlite3 import connect, OperationalError, Row -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple __all__ = "fetch", "initdb", "stow", "stowloc" @@ -25,6 +25,10 @@ SCHEMA = ( latitude real, longitude real, remainder text +)""", + """create table if not exists pmodmap ( + imei text not null unique, + pmod text not null )""", ) @@ -86,6 +90,17 @@ def stowloc(**kwargs: Dict[str, Any]) -> None: DB.commit() +def stowpmod(imei: str, pmod: str) -> None: + assert DB is not None + DB.execute( + """insert or replace into pmodmap + (imei, pmod) values (:imei, :pmod) + """, + {"imei": imei, "pmod": pmod}, + ) + DB.commit() + + def fetch(imei: str, backlog: int) -> List[Dict[str, Any]]: assert DB is not None cur = DB.cursor() @@ -103,3 +118,15 @@ def fetch(imei: str, backlog: int) -> List[Dict[str, Any]]: result.append(dic) cur.close() return list(reversed(result)) + + +def fetchpmod(imei: str) -> Optional[Any]: + assert DB is not None + ret = None + cur = DB.cursor() + cur.execute("select pmod from pmodmap where imei = ?", (imei,)) + result = cur.fetchone() + if result: + ret = result[0] + cur.close() + return ret diff --git a/loctrkd/protomodule.py b/loctrkd/protomodule.py index 1d61759..a1698f2 100644 --- a/loctrkd/protomodule.py +++ b/loctrkd/protomodule.py @@ -122,6 +122,8 @@ class ProtoClass(Protocol, metaclass=_MetaProto): class ProtoModule: + __name__: str + class Stream: def recv(self, segment: bytes) -> List[Union[bytes, str]]: ... @@ -171,3 +173,9 @@ class ProtoModule: @staticmethod def class_by_prefix(prefix: str) -> Union[Type[ProtoClass], List[str]]: ... + + @staticmethod + def make_response( + cmd: str, imei: str, **kwargs: Any + ) -> Optional[ProtoClass.Out]: + ... diff --git a/loctrkd/rectifier.py b/loctrkd/rectifier.py index 585eb79..5926377 100644 --- a/loctrkd/rectifier.py +++ b/loctrkd/rectifier.py @@ -68,10 +68,12 @@ def runserver(conf: ConfigParser) -> None: datetime.fromtimestamp(zmsg.when).astimezone(tz=timezone.utc), msg, ) - rect: Report = msg.rectified() + pmod, rect = msg.rectified() log.debug("rectified: %s", rect) if isinstance(rect, (CoordReport, StatusReport)): - zpub.send(Rept(imei=zmsg.imei, payload=rect.json).packed) + zpub.send( + Rept(imei=zmsg.imei, pmod=pmod, payload=rect.json).packed + ) elif isinstance(rect, HintReport): try: lat, lon, acc = qry.lookup( diff --git a/loctrkd/storage.py b/loctrkd/storage.py index 5ea3b2c..e1be227 100644 --- a/loctrkd/storage.py +++ b/loctrkd/storage.py @@ -7,7 +7,7 @@ from logging import getLogger import zmq from . import common -from .evstore import initdb, stow, stowloc +from .evstore import initdb, stow, stowloc, stowpmod from .zmsg import Bcast, Rept log = getLogger("loctrkd/storage") @@ -64,6 +64,8 @@ def runserver(conf: ConfigParser) -> None: rept = Rept(zrep.recv(zmq.NOBLOCK)) except zmq.Again: break + if rept.imei is not None and rept.pmod is not None: + stowpmod(rept.imei, rept.pmod) data = loads(rept.payload) log.debug("R IMEI %s %s", rept.imei, data) if data.pop("type") == "location": diff --git a/loctrkd/wsgateway.py b/loctrkd/wsgateway.py index e94845e..c5dcf5a 100644 --- a/loctrkd/wsgateway.py +++ b/loctrkd/wsgateway.py @@ -22,9 +22,9 @@ from wsproto.utilities import RemoteProtocolError import zmq from . import common -from .evstore import initdb, fetch +from .evstore import initdb, fetch, fetchpmod from .protomodule import ProtoModule -from .zmsg import Rept, rtopic +from .zmsg import Rept, Resp, rtopic log = getLogger("loctrkd/wsgateway") @@ -101,6 +101,9 @@ class Client: self.ready = False self.imeis: Set[str] = set() + def __str__(self) -> str: + return f"{self.__class__.__name__}(fd={self.sock.fileno()}, addr={self.addr})" + def close(self) -> None: log.debug("Closing fd %d", self.sock.fileno()) self.sock.close() @@ -247,6 +250,42 @@ class Clients: return result +def sendcmd(zpush: Any, wsmsg: Dict[str, Any]) -> Dict[str, Any]: + imei = wsmsg.pop("imei", None) + cmd = wsmsg.pop("type", None) + if imei is None or cmd is None: + log.info("Unhandled message %s %s %s", cmd, imei, wsmsg) + return { + "type": "cmdresult", + "imei": imei, + "result": "Did not get imei or cmd", + } + pmod = fetchpmod(imei) + if pmod is None: + log.info("Uknown type of recipient for %s %s %s", cmd, imei, wsmsg) + return { + "type": "cmdresult", + "imei": imei, + "result": "Type of the terminal is unknown", + } + tmsg = common.make_response(pmod, cmd, imei, **wsmsg) + if tmsg is None: + log.info("Could not make packet for %s %s %s", cmd, imei, wsmsg) + return { + "type": "cmdresult", + "imei": imei, + "result": f"{cmd} unimplemented for terminal protocol {pmod}", + } + resp = Resp(imei=imei, when=time(), packet=tmsg.packed) + log.debug("Response: %s", resp) + zpush.send(resp.packed) + return { + "type": "cmdresult", + "imei": imei, + "result": f"{cmd} sent to {imei}", + } + + def runserver(conf: ConfigParser) -> None: global htmlfile initdb(conf.get("storage", "dbfn")) @@ -255,6 +294,8 @@ def runserver(conf: ConfigParser) -> None: zctx = zmq.Context() # type: ignore zsub = zctx.socket(zmq.SUB) # type: ignore zsub.connect(conf.get("rectifier", "publishurl")) + zpush = zctx.socket(zmq.PUSH) # type: ignore + zpush.connect(conf.get("collector", "listenurl")) tcpl = socket(AF_INET6, SOCK_STREAM) tcpl.setblocking(False) tcpl.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) @@ -315,6 +356,8 @@ def runserver(conf: ConfigParser) -> None: for msg in backlog(imei, numback) ] ) + else: + tosend.append((clnt, sendcmd(zpush, wsmsg))) towrite.add(sk) elif fl & zmq.POLLOUT: log.debug("Write now open for fd %d", sk) diff --git a/loctrkd/zmsg.py b/loctrkd/zmsg.py index da0dc77..6539d5f 100644 --- a/loctrkd/zmsg.py +++ b/loctrkd/zmsg.py @@ -173,18 +173,21 @@ class Resp(_Zmsg): class Rept(_Zmsg): - """Broadcast Zzmq message with "rectified" proto-agnostic json data""" + """Broadcast zmq message with "rectified" proto-agnostic json data""" - KWARGS = (("imei", None), ("payload", "")) + KWARGS = (("imei", None), ("payload", ""), ("pmod", None)) @property def packed(self) -> bytes: return ( pack( - "16s", - "0000000000000000" + "16s16s", + b"0000000000000000" if self.imei is None else self.imei.encode(), + b" " + if self.pmod is None + else self.pmod.encode(), ) + self.payload.encode() ) @@ -194,4 +197,8 @@ class Rept(_Zmsg): self.imei = ( None if imei == b"0000000000000000" else imei.decode().strip("\0") ) - self.payload = buffer[16:].decode() + pmod = buffer[16:32] + self.pmod = ( + None if pmod == b" " else pmod.decode().strip("\0") + ) + self.payload = buffer[32:].decode() diff --git a/loctrkd/zx303proto.py b/loctrkd/zx303proto.py index aff3405..a7329c3 100755 --- a/loctrkd/zx303proto.py +++ b/loctrkd/zx303proto.py @@ -48,6 +48,7 @@ __all__ = ( "Respond", ) +MODNAME = __name__.split(".")[-1] PROTO_PREFIX: str = "ZX:" ### Deframer ### @@ -369,8 +370,8 @@ class _GPS_POSITIONING(GPS303Pkt): ttup = (tup[0] % 100,) + tup[1:6] return pack("BBBBBB", *ttup) - def rectified(self) -> CoordReport: # JSON-able dict - return CoordReport( + def rectified(self) -> Tuple[str, CoordReport]: # JSON-able dict + return MODNAME, CoordReport( devtime=str(self.devtime), battery_percentage=None, accuracy=None, @@ -419,8 +420,8 @@ class STATUS(GPS303Pkt): def out_encode(self) -> bytes: # Set interval in minutes return pack("B", self.upload_interval) - def rectified(self) -> StatusReport: - return StatusReport(battery_percentage=self.batt) + def rectified(self) -> Tuple[str, StatusReport]: + return MODNAME, StatusReport(battery_percentage=self.batt) class HIBERNATION(GPS303Pkt): # Server can send to send devicee to sleep @@ -500,8 +501,8 @@ class _WIFI_POSITIONING(GPS303Pkt): ] ) - def rectified(self) -> HintReport: - return HintReport( + def rectified(self) -> Tuple[str, HintReport]: + return MODNAME, HintReport( devtime=str(self.devtime), battery_percentage=None, mcc=self.mcc, @@ -895,3 +896,9 @@ def exposed_protos() -> List[Tuple[str, bool]]: for cls in CLASSES.values() if hasattr(cls, "rectified") ] + + +def make_response(cmd: str, imei: str, **kwargs: Any) -> Optional[GPS303Pkt]: + if cmd == "poweroff": + return HIBERNATION.Out() + return None -- 2.39.2