]> www.average.org Git - loctrkd.git/commitdiff
Implement sending commands from the web interface
authorEugene Crosser <crosser@average.org>
Sat, 26 Nov 2022 23:05:37 +0000 (00:05 +0100)
committerEugene Crosser <crosser@average.org>
Sun, 27 Nov 2022 20:14:01 +0000 (21:14 +0100)
loctrkd/beesure.py
loctrkd/common.py
loctrkd/evstore.py
loctrkd/protomodule.py
loctrkd/rectifier.py
loctrkd/storage.py
loctrkd/wsgateway.py
loctrkd/zmsg.py
loctrkd/zx303proto.py

index dcf16ba227fd7ca585baa9371595e19307917fe9..e782c736189ca5d74a3f0cc188c7973672660c13 100755 (executable)
@@ -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
index 941a93e008dbf793cad604d0df1fd86a81809b63..162fe0db07abbbd5a14e0cfbc429d93d193eb9d5 100644 (file)
@@ -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
index 85e8c9d25767de36fbf4e30444c9caa6f7bd6d5c..6d6edd018fc7d46747e9de26003870d222af0de6 100644 (file)
@@ -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
index 1d61759c19239b9e774e2bdc811eb6d53dd4fc19..a1698f2fde7ae52389a62b16dccf265122bacc93 100644 (file)
@@ -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]:
+        ...
index 585eb793d223ad37eab4fc7a2a04864f3d673c3e..5926377ee76ad4dc5665b72dec551a5e279bc661 100644 (file)
@@ -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(
index 5ea3b2c823c15618ed2845e6633b384fd2f93289..e1be227d8ef8b11d1b0b05586f4cba2dab19f2a1 100644 (file)
@@ -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":
index e94845eec4215e04797bde4e0dce43b05e8780ec..c5dcf5a785a03d13e8748bbfb441afec5b21492d 100644 (file)
@@ -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)
index da0dc77e39da6f38d7eaefe5adbdfe897f928b28..6539d5f18f67b8e3e88faa760ccbadb104af2861 100644 (file)
@@ -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()
index aff3405691baa596ca36ca90e00f7067880cef3f..a7329c316326187c950580ec11a65843981ace2d 100755 (executable)
@@ -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